;
-// @ts-expect-error - TS complains about the overload signature when we have more than 2 optional properties
async function withAuth(options: { ensureSignedIn: true }): Promise
;
-async function withAuth({ ensureSignedIn = false } = {}) {
+async function withAuth({ ensureSignedIn = false } = {}): Promise {
const session = await getSessionFromHeader();
if (!session) {
diff --git a/tsconfig.json b/tsconfig.json
index 8595c61..d41140b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -12,11 +12,11 @@
"alwaysStrict": true,
"skipLibCheck": true,
"outDir": "./dist/esm",
+ "declarationDir": "./dist/esm/types",
"module": "ES2020",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
- "declarationDir": "./dist/types"
},
- "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "jest.config.ts", "jest.setup.ts"]
+ "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "jest.config.ts", "jest.setup.ts", "/dist/**/*"]
}
From 0bc25b25c8da8cfeab8af40e64d6290f629cec8c Mon Sep 17 00:00:00 2001
From: Paul Asjes
Date: Tue, 10 Dec 2024 14:37:14 +0100
Subject: [PATCH 15/19] Update readme and make sure refreshAuth works as
expected
---
README.md | 52 +++++++++++++++++++++++++----
src/components/authkit-provider.tsx | 9 +----
src/components/index.ts | 4 +--
src/session.ts | 19 ++++++++---
4 files changed, 62 insertions(+), 22 deletions(-)
diff --git a/README.md b/README.md
index 6cfa4c5..06e1db4 100644
--- a/README.md
+++ b/README.md
@@ -150,7 +150,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
### Get the current user in a server component
-For pages where you want to display a signed-in and signed-out view, use `withAuth` to retrieve the user profile from WorkOS.
+For pages where you want to display a signed-in and signed-out view, use `withAuth` to retrieve the user session from WorkOS.
```jsx
import Link from 'next/link';
@@ -198,6 +198,7 @@ For client components, use the `useAuth` hook to get the current user session.
import { useAuth } from '@workos-inc/authkit-nextjs/components';
export default function MyComponent() {
+ // Retrieves the user from the session or returns `null` if no user is signed in
const { user, loading } = useAuth();
if (loading) {
@@ -222,6 +223,45 @@ const { user, loading } = useAuth({ ensureSignedIn: true });
Enabling `ensureSignedIn` will redirect users to AuthKit if they attempt to access the page without being authenticated.
+### Refreshing the session
+
+Use the `refreshSession` method in a server action or route handler to fetch the latest session details, including any changes to the user's roles or permissions.
+
+The `organizationId` parameter can be passed to `refreshSession` in order to switch the session to a different organization. If the current session is not authorized for the next organization, an appropriate [authentication error](https://workos.com/docs/reference/user-management/authentication-errors) will be returned.
+
+In client components, you can refresh the session with the `refreshAuth` hook.
+
+```tsx
+'use client';
+
+import { useAuth } from '@workos-inc/authkit-nextjs/components';
+import React, { useEffect } from 'react';
+
+export function SwitchOrganizationButton() {
+ const { user, organizationId, loading, refreshAuth } = useAuth();
+
+ useEffect(() => {
+ // This will log out the new organizationId after refreshing the session
+ console.log('organizationId', organizationId);
+ }, [organizationId]);
+
+ if (loading) {
+ return Loading...
;
+ }
+
+ const handleRefreshSession = async () => {
+ // Provide the organizationId to switch to
+ await refreshAuth({ organizationId: 'org_123' });
+ };
+
+ if (user) {
+ return ;
+ } else {
+ return Not signed in
;
+ }
+}
+```
+
### Middleware auth
The default behavior of this library is to request authentication via the `withAuth` method on a per-page basis. There are some use cases where you don't want to call `withAuth` (e.g. you don't need user data for your page) or if you'd prefer a "secure by default" approach where every route defined in your middleware matcher is protected unless specified otherwise. In those cases you can opt-in to use middleware auth instead:
@@ -317,12 +357,6 @@ export default async function HomePage() {
}
```
-### Refreshing the session
-
-Use the `refreshSession` method in a server action or route handler to fetch the latest session details, including any changes to the user's roles or permissions.
-
-The `organizationId` parameter can be passed to `refreshSession` in order to switch the session to a different organization. If the current session is not authorized for the next organization, an appropriate [authentication error](https://workos.com/docs/reference/user-management/authentication-errors) will be returned.
-
### Sign up paths
The `signUpPaths` option can be passed to `authkitMiddleware` to specify paths that should use the 'sign-up' screen hint when redirecting to AuthKit. This is useful for cases where you want a path that mandates authentication to be treated as a sign up page.
@@ -350,3 +384,7 @@ export default authkitMiddleware({ debug: true });
#### NEXT_REDIRECT error when using try/catch blocks
Wrapping a `withAuth({ ensureSignedIn: true })` call in a try/catch block will cause a `NEXT_REDIRECT` error. This is because `withAuth` will attempt to redirect the user to AuthKit if no session is detected and redirects in Next must be [called outside a try/catch](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#redirecting).
+
+#### Module build failed: UnhandledSchemeError: Reading from "node:crypto" is not handled by plugins (Unhandled scheme).
+
+You may encounter this error if you attempt to import server side code from authkit-nextjs into a client component. Likely you are using `withAuth` in a client component instead of the `useAuth` hook. Either move the code to a server component or use the `useAuth` hook.
diff --git a/src/components/authkit-provider.tsx b/src/components/authkit-provider.tsx
index 466545b..41e1f8e 100644
--- a/src/components/authkit-provider.tsx
+++ b/src/components/authkit-provider.tsx
@@ -68,6 +68,7 @@ export const AuthKitProvider = ({ children, onSessionExpired }: AuthKitProviderP
try {
setLoading(true);
const auth = await refreshAuthAction({ ensureSignedIn, organizationId });
+
setUser(auth.user);
setSessionId(auth.sessionId);
setOrganizationId(auth.organizationId);
@@ -172,11 +173,3 @@ export function useAuth() {
}
return context;
}
-
-export function refreshAuth() {
- const context = useContext(AuthContext);
- if (!context) {
- throw new Error('refreshAuth must be used within an AuthKitProvider');
- }
- return context;
-}
diff --git a/src/components/index.ts b/src/components/index.ts
index c0016f3..6f6aeb5 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1,4 +1,4 @@
import { Impersonation } from './impersonation.js';
-import { AuthKitProvider, useAuth, refreshAuth } from './authkit-provider.js';
+import { AuthKitProvider, useAuth } from './authkit-provider.js';
-export { Impersonation, AuthKitProvider, useAuth, refreshAuth };
+export { Impersonation, AuthKitProvider, useAuth };
diff --git a/src/session.ts b/src/session.ts
index 96cd963..e680bbe 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -186,12 +186,21 @@ async function refreshSession({
const { org_id: organizationIdFromAccessToken } = decodeJwt(session.accessToken);
- const { accessToken, refreshToken, user, impersonator } = await workos.userManagement.authenticateWithRefreshToken({
- clientId: WORKOS_CLIENT_ID,
- refreshToken: session.refreshToken,
- organizationId: nextOrganizationId ?? organizationIdFromAccessToken,
- });
+ let refreshResult;
+
+ try {
+ refreshResult = await workos.userManagement.authenticateWithRefreshToken({
+ clientId: WORKOS_CLIENT_ID,
+ refreshToken: session.refreshToken,
+ organizationId: nextOrganizationId ?? organizationIdFromAccessToken,
+ });
+ } catch (error) {
+ throw new Error(`Failed to refresh session: ${error instanceof Error ? error.message : String(error)}`, {
+ cause: error,
+ });
+ }
+ const { accessToken, refreshToken, user, impersonator } = refreshResult;
// Encrypt session with new access and refresh tokens
const encryptedSession = await encryptSession({
accessToken,
From 363685dcd278ba07abc550a7a815521518c7471e Mon Sep 17 00:00:00 2001
From: Paul Asjes
Date: Tue, 10 Dec 2024 15:49:49 +0100
Subject: [PATCH 16/19] Tests
---
README.md | 9 +-
__tests__/actions.spec.ts | 49 +++++-
__tests__/authkit-provider.spec.tsx | 243 +++++++++++++++++++++++++---
__tests__/impersonation.spec.tsx | 91 +++++++----
__tests__/session.spec.ts | 11 ++
src/components/authkit-provider.tsx | 20 +--
src/session.ts | 2 +-
7 files changed, 351 insertions(+), 74 deletions(-)
diff --git a/README.md b/README.md
index 06e1db4..4ce43cf 100644
--- a/README.md
+++ b/README.md
@@ -250,8 +250,13 @@ export function SwitchOrganizationButton() {
}
const handleRefreshSession = async () => {
- // Provide the organizationId to switch to
- await refreshAuth({ organizationId: 'org_123' });
+ const result = await refreshAuth({
+ // Provide the organizationId to switch to
+ organizationId: 'org_123',
+ });
+ if (result?.error) {
+ console.log('Error refreshing session:', result.error);
+ }
};
if (user) {
diff --git a/__tests__/actions.spec.ts b/__tests__/actions.spec.ts
index 976d2e7..404757d 100644
--- a/__tests__/actions.spec.ts
+++ b/__tests__/actions.spec.ts
@@ -1,10 +1,31 @@
-import { checkSessionAction, handleSignOutAction } from '../src/actions.js';
+import {
+ checkSessionAction,
+ handleSignOutAction,
+ getOrganizationAction,
+ getAuthAction,
+ refreshAuthAction,
+} from '../src/actions.js';
import { signOut } from '../src/auth.js';
+import { workos } from '../src/workos.js';
+import { withAuth, refreshSession } from '../src/session.js';
jest.mock('../src/auth.js', () => ({
signOut: jest.fn().mockResolvedValue(true),
}));
+jest.mock('../src/workos.js', () => ({
+ workos: {
+ organizations: {
+ getOrganization: jest.fn().mockResolvedValue({ id: 'org_123', name: 'Test Org' }),
+ },
+ },
+}));
+
+jest.mock('../src/session.js', () => ({
+ withAuth: jest.fn().mockResolvedValue({ user: 'testUser' }),
+ refreshSession: jest.fn().mockResolvedValue({ session: 'newSession' }),
+}));
+
describe('actions', () => {
describe('checkSessionAction', () => {
it('should return true for authenticated users', async () => {
@@ -19,4 +40,30 @@ describe('actions', () => {
expect(signOut).toHaveBeenCalled();
});
});
+
+ describe('getOrganizationAction', () => {
+ it('should return organization details', async () => {
+ const organizationId = 'org_123';
+ const result = await getOrganizationAction(organizationId);
+ expect(workos.organizations.getOrganization).toHaveBeenCalledWith(organizationId);
+ expect(result).toEqual({ id: 'org_123', name: 'Test Org' });
+ });
+ });
+
+ describe('getAuthAction', () => {
+ it('should return auth details', async () => {
+ const result = await getAuthAction();
+ expect(withAuth).toHaveBeenCalled();
+ expect(result).toEqual({ user: 'testUser' });
+ });
+ });
+
+ describe('refreshAuthAction', () => {
+ it('should refresh session', async () => {
+ const params = { ensureSignedIn: true, organizationId: 'org_123' };
+ const result = await refreshAuthAction(params);
+ expect(refreshSession).toHaveBeenCalledWith(params);
+ expect(result).toEqual({ session: 'newSession' });
+ });
+ });
});
diff --git a/__tests__/authkit-provider.spec.tsx b/__tests__/authkit-provider.spec.tsx
index 084155f..5175072 100644
--- a/__tests__/authkit-provider.spec.tsx
+++ b/__tests__/authkit-provider.spec.tsx
@@ -1,11 +1,13 @@
import React from 'react';
-import { render, waitFor } from '@testing-library/react';
+import { render, waitFor, act } from '@testing-library/react';
import '@testing-library/jest-dom';
-import { AuthKitProvider } from '../src/components/authkit-provider.js';
-import { checkSessionAction } from '../src/actions.js';
+import { AuthKitProvider, useAuth } from '../src/components/authkit-provider.js';
+import { checkSessionAction, getAuthAction, refreshAuthAction } from '../src/actions.js';
jest.mock('../src/actions', () => ({
checkSessionAction: jest.fn(),
+ getAuthAction: jest.fn(),
+ refreshAuthAction: jest.fn(),
}));
describe('AuthKitProvider', () => {
@@ -13,12 +15,14 @@ describe('AuthKitProvider', () => {
jest.clearAllMocks();
});
- it('should render children', () => {
- const { getByText } = render(
-
- Test Child
- ,
- );
+ it('should render children', async () => {
+ const { getByText } = await act(async () => {
+ return render(
+
+ Test Child
+ ,
+ );
+ });
expect(getByText('Test Child')).toBeInTheDocument();
});
@@ -26,11 +30,13 @@ describe('AuthKitProvider', () => {
it('should do nothing if onSessionExpired is false', async () => {
jest.spyOn(window, 'addEventListener');
- render(
-
- Test Child
- ,
- );
+ await act(async () => {
+ render(
+
+ Test Child
+ ,
+ );
+ });
// expect window to not have an event listener
expect(window.addEventListener).not.toHaveBeenCalled();
@@ -46,8 +52,10 @@ describe('AuthKitProvider', () => {
,
);
- // Simulate visibility change
- window.dispatchEvent(new Event('visibilitychange'));
+ act(() => {
+ // Simulate visibility change
+ window.dispatchEvent(new Event('visibilitychange'));
+ });
await waitFor(() => {
expect(onSessionExpired).toHaveBeenCalled();
@@ -64,9 +72,11 @@ describe('AuthKitProvider', () => {
,
);
- // Simulate visibility change twice
- window.dispatchEvent(new Event('visibilitychange'));
- window.dispatchEvent(new Event('visibilitychange'));
+ act(() => {
+ // Simulate visibility change twice
+ window.dispatchEvent(new Event('visibilitychange'));
+ window.dispatchEvent(new Event('visibilitychange'));
+ });
await waitFor(() => {
expect(onSessionExpired).toHaveBeenCalledTimes(1);
@@ -84,8 +94,10 @@ describe('AuthKitProvider', () => {
,
);
- // Simulate visibility change
- window.dispatchEvent(new Event('visibilitychange'));
+ act(() => {
+ // Simulate visibility change
+ window.dispatchEvent(new Event('visibilitychange'));
+ });
await waitFor(() => {
expect(onSessionExpired).not.toHaveBeenCalled();
@@ -108,8 +120,10 @@ describe('AuthKitProvider', () => {
,
);
- // Simulate visibility change
- window.dispatchEvent(new Event('visibilitychange'));
+ act(() => {
+ // Simulate visibility change
+ window.dispatchEvent(new Event('visibilitychange'));
+ });
await waitFor(() => {
expect(window.location.reload).toHaveBeenCalled();
@@ -136,8 +150,10 @@ describe('AuthKitProvider', () => {
,
);
- // Simulate visibility change
- window.dispatchEvent(new Event('visibilitychange'));
+ act(() => {
+ // Simulate visibility change
+ window.dispatchEvent(new Event('visibilitychange'));
+ });
await waitFor(() => {
expect(onSessionExpired).not.toHaveBeenCalled();
@@ -147,3 +163,180 @@ describe('AuthKitProvider', () => {
window.location = originalLocation;
});
});
+
+describe('useAuth', () => {
+ it('should throw error when used outside of AuthKitProvider', () => {
+ const TestComponent = () => {
+ const auth = useAuth();
+ return {auth.user?.email}
;
+ };
+
+ // Suppress console.error for this test since we expect an error
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ expect(() => {
+ render();
+ }).toThrow('useAuth must be used within an AuthKitProvider');
+
+ consoleSpy.mockRestore();
+ });
+
+ it('should provide auth context values when used within AuthKitProvider', async () => {
+ (getAuthAction as jest.Mock).mockResolvedValueOnce({
+ user: { email: 'test@example.com' },
+ sessionId: 'test-session',
+ organizationId: 'test-org',
+ role: 'admin',
+ permissions: ['read', 'write'],
+ entitlements: ['feature1'],
+ impersonator: { email: 'admin@example.com' },
+ oauthTokens: { access_token: 'token123' },
+ accessToken: 'access123',
+ });
+
+ const TestComponent = () => {
+ const auth = useAuth();
+ return (
+
+
{auth.loading.toString()}
+
{auth.user?.email}
+
{auth.sessionId}
+
{auth.organizationId}
+
+ );
+ };
+
+ const { getByTestId } = render(
+
+
+ ,
+ );
+
+ // Initially loading
+ expect(getByTestId('loading')).toHaveTextContent('true');
+
+ // Wait for auth to load
+ await waitFor(() => {
+ expect(getByTestId('loading')).toHaveTextContent('false');
+ expect(getByTestId('email')).toHaveTextContent('test@example.com');
+ expect(getByTestId('session')).toHaveTextContent('test-session');
+ expect(getByTestId('org')).toHaveTextContent('test-org');
+ });
+ });
+
+ it('should handle auth methods (getAuth and refreshAuth)', async () => {
+ const mockAuth = {
+ user: { email: 'test@example.com' },
+ sessionId: 'test-session',
+ };
+
+ (getAuthAction as jest.Mock).mockResolvedValueOnce(mockAuth);
+ (refreshAuthAction as jest.Mock).mockResolvedValueOnce({
+ ...mockAuth,
+ sessionId: 'new-session',
+ });
+
+ const TestComponent = () => {
+ const auth = useAuth();
+ return (
+
+
{auth.sessionId}
+
+
+ );
+ };
+
+ const { getByTestId, getByRole } = render(
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(getByTestId('session')).toHaveTextContent('test-session');
+ });
+
+ // Test refresh
+ act(() => {
+ getByRole('button').click();
+ });
+
+ await waitFor(() => {
+ expect(getByTestId('session')).toHaveTextContent('new-session');
+ });
+ });
+
+ it('should receive an error when refreshAuth fails with an error', async () => {
+ (refreshAuthAction as jest.Mock).mockRejectedValueOnce(new Error('Refresh failed'));
+
+ let error: string | undefined;
+
+ const TestComponent = () => {
+ const auth = useAuth();
+ return (
+
+
{auth.sessionId}
+
+
+ );
+ };
+
+ const { getByRole } = render(
+
+
+ ,
+ );
+
+ act(() => {
+ getByRole('button').click();
+ });
+
+ await waitFor(() => {
+ expect(error).toBe('Refresh failed');
+ });
+ });
+
+ it('should receive an error when refreshAuth fails with an errstringor', async () => {
+ (refreshAuthAction as jest.Mock).mockRejectedValueOnce('Refresh failed');
+
+ let error: string | undefined;
+
+ const TestComponent = () => {
+ const auth = useAuth();
+ return (
+
+
{auth.sessionId}
+
+
+ );
+ };
+
+ const { getByRole } = render(
+
+
+ ,
+ );
+
+ act(() => {
+ getByRole('button').click();
+ });
+
+ await waitFor(() => {
+ expect(error).toBe('Refresh failed');
+ });
+ });
+});
diff --git a/__tests__/impersonation.spec.tsx b/__tests__/impersonation.spec.tsx
index 808943d..382da78 100644
--- a/__tests__/impersonation.spec.tsx
+++ b/__tests__/impersonation.spec.tsx
@@ -1,19 +1,19 @@
-import { render } from '@testing-library/react';
+import { render, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Impersonation } from '../src/components/impersonation.js';
-import { withAuth } from '../src/session.js';
-import { workos } from '../src/workos.js';
+import { useAuth } from '../src/components/authkit-provider.js';
+import { getOrganizationAction } from '../src/actions.js';
+import * as React from 'react';
-jest.mock('../src/session', () => ({
- withAuth: jest.fn(),
+// Mock the useAuth hook
+jest.mock('../src/components/authkit-provider', () => ({
+ useAuth: jest.fn(),
}));
-jest.mock('../src/workos', () => ({
- workos: {
- organizations: {
- getOrganization: jest.fn(),
- },
- },
+// Mock the getOrganizationAction
+jest.mock('../src/actions', () => ({
+ getOrganizationAction: jest.fn(),
+ handleSignOutAction: jest.fn(),
}));
describe('Impersonation', () => {
@@ -21,77 +21,98 @@ describe('Impersonation', () => {
jest.clearAllMocks();
});
- it('should return null if not impersonating', async () => {
- (withAuth as jest.Mock).mockResolvedValue({
+ it('should return null if not impersonating', () => {
+ (useAuth as jest.Mock).mockReturnValue({
impersonator: null,
- user: { id: '123' },
+ user: { id: '123', email: 'user@example.com' },
organizationId: null,
+ loading: false,
});
- const { container } = await render(await Impersonation({}));
+ const { container } = render();
expect(container).toBeEmptyDOMElement();
});
- it('should render impersonation banner when impersonating', async () => {
- (withAuth as jest.Mock).mockResolvedValue({
+ it('should return null if loading', () => {
+ (useAuth as jest.Mock).mockReturnValue({
impersonator: { email: 'admin@example.com' },
- user: { id: '123' },
+ user: { id: '123', email: 'user@example.com' },
organizationId: null,
+ loading: true,
});
- const { container } = await render(await Impersonation({}));
+ const { container } = render();
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('should render impersonation banner when impersonating', () => {
+ (useAuth as jest.Mock).mockReturnValue({
+ impersonator: { email: 'admin@example.com' },
+ user: { id: '123', email: 'user@example.com' },
+ organizationId: null,
+ loading: false,
+ });
+
+ const { container } = render();
expect(container.querySelector('[data-workos-impersonation-root]')).toBeInTheDocument();
});
it('should render with organization info when organizationId is provided', async () => {
- (withAuth as jest.Mock).mockResolvedValue({
+ (useAuth as jest.Mock).mockReturnValue({
impersonator: { email: 'admin@example.com' },
- user: { id: '123' },
+ user: { id: '123', email: 'user@example.com' },
organizationId: 'org_123',
+ loading: false,
});
- (workos.organizations.getOrganization as jest.Mock).mockResolvedValue({
+ (getOrganizationAction as jest.Mock).mockResolvedValue({
id: 'org_123',
name: 'Test Org',
});
- const { container } = await render(await Impersonation({}));
+ const { container } = await act(async () => {
+ return render();
+ });
+
expect(container.querySelector('[data-workos-impersonation-root]')).toBeInTheDocument();
});
- it('should render at the bottom by default', async () => {
- (withAuth as jest.Mock).mockResolvedValue({
+ it('should render at the bottom by default', () => {
+ (useAuth as jest.Mock).mockReturnValue({
impersonator: { email: 'admin@example.com' },
- user: { id: '123' },
+ user: { id: '123', email: 'user@example.com' },
organizationId: null,
+ loading: false,
});
- const { container } = await render(await Impersonation({}));
+ const { container } = render();
const banner = container.querySelector('[data-workos-impersonation-root] > div:nth-child(2)');
expect(banner).toHaveStyle({ bottom: 'var(--wi-s)' });
});
- it('should render at the top when side prop is "top"', async () => {
- (withAuth as jest.Mock).mockResolvedValue({
+ it('should render at the top when side prop is "top"', () => {
+ (useAuth as jest.Mock).mockReturnValue({
impersonator: { email: 'admin@example.com' },
- user: { id: '123' },
+ user: { id: '123', email: 'user@example.com' },
organizationId: null,
+ loading: false,
});
- const { container } = await render(await Impersonation({ side: 'top' }));
+ const { container } = render();
const banner = container.querySelector('[data-workos-impersonation-root] > div:nth-child(2)');
expect(banner).toHaveStyle({ top: 'var(--wi-s)' });
});
- it('should merge custom styles with default styles', async () => {
- (withAuth as jest.Mock).mockResolvedValue({
+ it('should merge custom styles with default styles', () => {
+ (useAuth as jest.Mock).mockReturnValue({
impersonator: { email: 'admin@example.com' },
- user: { id: '123' },
+ user: { id: '123', email: 'user@example.com' },
organizationId: null,
+ loading: false,
});
const customStyle = { backgroundColor: 'red' };
- const { container } = await render(await Impersonation({ style: customStyle }));
+ const { container } = render();
const root = container.querySelector('[data-workos-impersonation-root]');
expect(root).toHaveStyle({ backgroundColor: 'red' });
});
diff --git a/__tests__/session.spec.ts b/__tests__/session.spec.ts
index 344d43e..62f0058 100644
--- a/__tests__/session.spec.ts
+++ b/__tests__/session.spec.ts
@@ -116,6 +116,17 @@ describe('session.ts', () => {
);
});
+ it('should throw an error if the route is not covered by the middleware and there is no URL in the headers', async () => {
+ const nextHeaders = await headers();
+ nextHeaders.delete('x-workos-middleware');
+
+ await expect(async () => {
+ await withAuth({ ensureSignedIn: true });
+ }).rejects.toThrow(
+ "You are calling 'withAuth' on a route that isn’t covered by the AuthKit middleware. Make sure it is running on all paths you are calling 'withAuth' from by updating your middleware config in 'middleware.(js|ts)'.",
+ );
+ });
+
it('should throw an error if the URL is not found in the headers', async () => {
const nextHeaders = await headers();
nextHeaders.delete('x-url');
diff --git a/src/components/authkit-provider.tsx b/src/components/authkit-provider.tsx
index 41e1f8e..d46d8db 100644
--- a/src/components/authkit-provider.tsx
+++ b/src/components/authkit-provider.tsx
@@ -16,7 +16,7 @@ type AuthContextType = {
accessToken: string | undefined;
loading: boolean;
getAuth: (options?: { ensureSignedIn?: boolean }) => Promise;
- refreshAuth: (options?: { ensureSignedIn?: boolean; organizationId?: string }) => Promise;
+ refreshAuth: (options?: { ensureSignedIn?: boolean; organizationId?: string }) => Promise;
};
const AuthContext = createContext(undefined);
@@ -56,6 +56,14 @@ export const AuthKitProvider = ({ children, onSessionExpired }: AuthKitProviderP
setAccessToken(auth.accessToken);
} catch (error) {
setUser(null);
+ setSessionId(undefined);
+ setOrganizationId(undefined);
+ setRole(undefined);
+ setPermissions(undefined);
+ setEntitlements(undefined);
+ setImpersonator(undefined);
+ setOauthTokens(undefined);
+ setAccessToken(undefined);
} finally {
setLoading(false);
}
@@ -79,15 +87,7 @@ export const AuthKitProvider = ({ children, onSessionExpired }: AuthKitProviderP
setOauthTokens(auth.oauthTokens);
setAccessToken(auth.accessToken);
} catch (error) {
- setUser(null);
- setSessionId(undefined);
- setOrganizationId(undefined);
- setRole(undefined);
- setPermissions(undefined);
- setEntitlements(undefined);
- setImpersonator(undefined);
- setOauthTokens(undefined);
- setAccessToken(undefined);
+ return error instanceof Error ? { error: error.message } : { error: String(error) };
} finally {
setLoading(false);
}
diff --git a/src/session.ts b/src/session.ts
index e680bbe..230fbf6 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -375,7 +375,7 @@ async function getSessionFromHeader(): Promise {
if (!hasMiddleware) {
const url = headersList.get('x-url');
throw new Error(
- `You are calling 'withAuth' on ${url} that isn’t covered by the AuthKit middleware. Make sure it is running on all paths you are calling 'withAuth' from by updating your middleware config in 'middleware.(js|ts)'.`,
+ `You are calling 'withAuth' on ${url ?? 'a route'} that isn’t covered by the AuthKit middleware. Make sure it is running on all paths you are calling 'withAuth' from by updating your middleware config in 'middleware.(js|ts)'.`,
);
}
From b636c4c5c8b66c394164a19d8b0bed3f74c17b4d Mon Sep 17 00:00:00 2001
From: Paul Asjes
Date: Tue, 10 Dec 2024 16:21:50 +0100
Subject: [PATCH 17/19] Fix coverage
---
src/env-variables.ts | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/env-variables.ts b/src/env-variables.ts
index 58536b1..e161f9a 100644
--- a/src/env-variables.ts
+++ b/src/env-variables.ts
@@ -1,15 +1,20 @@
+/* istanbul ignore file */
+
function getEnvVariable(name: string): string | undefined {
return process.env[name];
}
+// Optional env variables
const WORKOS_API_HOSTNAME = getEnvVariable('WORKOS_API_HOSTNAME');
const WORKOS_API_HTTPS = getEnvVariable('WORKOS_API_HTTPS');
-const WORKOS_API_KEY = getEnvVariable('WORKOS_API_KEY') ?? '';
const WORKOS_API_PORT = getEnvVariable('WORKOS_API_PORT');
-const WORKOS_CLIENT_ID = getEnvVariable('WORKOS_CLIENT_ID') ?? '';
const WORKOS_COOKIE_DOMAIN = getEnvVariable('WORKOS_COOKIE_DOMAIN');
const WORKOS_COOKIE_MAX_AGE = getEnvVariable('WORKOS_COOKIE_MAX_AGE');
const WORKOS_COOKIE_NAME = getEnvVariable('WORKOS_COOKIE_NAME');
+
+// Required env variables
+const WORKOS_API_KEY = getEnvVariable('WORKOS_API_KEY') ?? '';
+const WORKOS_CLIENT_ID = getEnvVariable('WORKOS_CLIENT_ID') ?? '';
const WORKOS_COOKIE_PASSWORD = getEnvVariable('WORKOS_COOKIE_PASSWORD') ?? '';
const WORKOS_REDIRECT_URI = process.env.NEXT_PUBLIC_WORKOS_REDIRECT_URI ?? '';
From e6cacb7fe34e351882c6a56c381b101810fd2263 Mon Sep 17 00:00:00 2001
From: Paul Asjes
Date: Fri, 27 Dec 2024 17:44:20 +0200
Subject: [PATCH 18/19] Remove oauthTokens from provider
---
src/actions.ts | 6 ------
src/components/authkit-provider.tsx | 8 +-------
src/components/impersonation.tsx | 8 --------
3 files changed, 1 insertion(+), 21 deletions(-)
diff --git a/src/actions.ts b/src/actions.ts
index 35ca599..1c6f5be 100644
--- a/src/actions.ts
+++ b/src/actions.ts
@@ -1,11 +1,8 @@
'use server';
import { signOut } from './auth.js';
-<<<<<<< HEAD
import { refreshSession, withAuth } from './session.js';
import { workos } from './workos.js';
-=======
->>>>>>> main
/**
* This action is only accessible to authenticated users,
@@ -19,7 +16,6 @@ export const checkSessionAction = async () => {
export const handleSignOutAction = async () => {
await signOut();
};
-<<<<<<< HEAD
export const getOrganizationAction = async (organizationId: string) => {
return await workos.organizations.getOrganization(organizationId);
@@ -38,5 +34,3 @@ export const refreshAuthAction = async ({
}) => {
return await refreshSession({ ensureSignedIn, organizationId });
};
-=======
->>>>>>> main
diff --git a/src/components/authkit-provider.tsx b/src/components/authkit-provider.tsx
index d46d8db..b61f7f4 100644
--- a/src/components/authkit-provider.tsx
+++ b/src/components/authkit-provider.tsx
@@ -2,7 +2,7 @@
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import { checkSessionAction, getAuthAction, refreshAuthAction } from '../actions.js';
-import type { Impersonator, OauthTokens, User } from '@workos-inc/node';
+import type { Impersonator, User } from '@workos-inc/node';
type AuthContextType = {
user: User | null;
@@ -12,7 +12,6 @@ type AuthContextType = {
permissions: string[] | undefined;
entitlements: string[] | undefined;
impersonator: Impersonator | undefined;
- oauthTokens: OauthTokens | undefined;
accessToken: string | undefined;
loading: boolean;
getAuth: (options?: { ensureSignedIn?: boolean }) => Promise;
@@ -38,7 +37,6 @@ export const AuthKitProvider = ({ children, onSessionExpired }: AuthKitProviderP
const [permissions, setPermissions] = useState(undefined);
const [entitlements, setEntitlements] = useState(undefined);
const [impersonator, setImpersonator] = useState(undefined);
- const [oauthTokens, setOauthTokens] = useState(undefined);
const [accessToken, setAccessToken] = useState(undefined);
const [loading, setLoading] = useState(true);
@@ -52,7 +50,6 @@ export const AuthKitProvider = ({ children, onSessionExpired }: AuthKitProviderP
setPermissions(auth.permissions);
setEntitlements(auth.entitlements);
setImpersonator(auth.impersonator);
- setOauthTokens(auth.oauthTokens);
setAccessToken(auth.accessToken);
} catch (error) {
setUser(null);
@@ -62,7 +59,6 @@ export const AuthKitProvider = ({ children, onSessionExpired }: AuthKitProviderP
setPermissions(undefined);
setEntitlements(undefined);
setImpersonator(undefined);
- setOauthTokens(undefined);
setAccessToken(undefined);
} finally {
setLoading(false);
@@ -84,7 +80,6 @@ export const AuthKitProvider = ({ children, onSessionExpired }: AuthKitProviderP
setPermissions(auth.permissions);
setEntitlements(auth.entitlements);
setImpersonator(auth.impersonator);
- setOauthTokens(auth.oauthTokens);
setAccessToken(auth.accessToken);
} catch (error) {
return error instanceof Error ? { error: error.message } : { error: String(error) };
@@ -154,7 +149,6 @@ export const AuthKitProvider = ({ children, onSessionExpired }: AuthKitProviderP
permissions,
entitlements,
impersonator,
- oauthTokens,
accessToken,
loading,
getAuth,
diff --git a/src/components/impersonation.tsx b/src/components/impersonation.tsx
index bd1d43c..6edf1bd 100644
--- a/src/components/impersonation.tsx
+++ b/src/components/impersonation.tsx
@@ -1,19 +1,11 @@
'use client';
import * as React from 'react';
-<<<<<<< HEAD:src/components/impersonation.tsx
import { Button } from './button.js';
import { MinMaxButton } from './min-max-button.js';
import { getOrganizationAction, handleSignOutAction } from '../actions.js';
import type { Organization } from '@workos-inc/node';
import { useAuth } from './authkit-provider.js';
-=======
-import { withAuth } from './session.js';
-import { workos } from './workos.js';
-import { Button } from './button.js';
-import { MinMaxButton } from './min-max-button.js';
-import { handleSignOutAction } from './actions.js';
->>>>>>> main:src/impersonation.tsx
interface ImpersonationProps extends React.ComponentPropsWithoutRef<'div'> {
side?: 'top' | 'bottom';
From 05d8294d9e1d3329953c2078cf6436e423adfbcd Mon Sep 17 00:00:00 2001
From: Paul Asjes
Date: Fri, 27 Dec 2024 19:46:27 +0200
Subject: [PATCH 19/19] Exclude all test files from build
---
tsconfig.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tsconfig.json b/tsconfig.json
index d41140b..b436740 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -17,6 +17,6 @@
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
},
- "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "jest.config.ts", "jest.setup.ts", "/dist/**/*"]
+ "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "jest.config.ts", "jest.setup.ts", "/dist/**/*", "__tests__/**/*"]
}