@freshheads/react-auth
v0.0.6
Published
A type-safe React authentication library for Next.js and react applications with built-in role-based access control (RBAC) and JWT token management. Based on our own authentication backend.
Readme
@freshheads/react-auth
A type-safe React authentication library for Next.js and react applications with built-in role-based access control (RBAC) and JWT token management. Based on our own authentication backend.
Features
- 🔐 Authentication Management - Built-in login/logout functionality with JWT token handling
- ✅ Auth Levels - Supports
authLevelstates on session (partial,setup_required,full) - 🔒 Optional MFA Redirect Flow - Redirect partial sessions to an MFA route until challenge is completed
- 🛡️ Role-Based Access Control (RBAC) - Define route access based on user roles
- 🎯 Type-Safe - Full TypeScript support with generic types for routes and roles
- 🚀 Next.js Middleware Integration - Server-side route protection
- 🪝 React Hooks - Easy-to-use hooks for authentication state and utilities
- 🍪 Cookie Management - Automatic JWT cookie handling
Installation
npm install @freshheads/react-authPeer Dependencies
This package requires the following peer dependencies:
npm install react react-dom next typescriptConfiguration Options
ReactAuthConfig
| Property | Type | Description |
|----------|------|-------------|
| rbacRoutes | RbacRoutes | Maps roles to their accessible routes and landing pages |
| defaultRoutes | { login: ValidRoute, landing: ValidRoute } | Default login and landing routes |
| openRoutes | ValidRoute[] | Routes accessible to everyone (authenticated or not) |
| unauthenticatedRoutes | ValidRoute[] | Routes only accessible when NOT authenticated |
| hiddenRoutePrefix | string | Prefix to ignore for route matching (e.g., {/:locale}) |
| mfa | { landingRoute?: ValidRoute, allowedRoutes?: ValidRoute[] } or true | Optional MFA settings for partial sessions, true enables with default settings {landingRoute: '/mfa'} |
| fetchMutator | FetchMutator | Optional fetch wrapper used by adapters/session/hooks |
| session | SessionStrategy | Session strategy for getSession |
| adapters | AuthAdapter[] | Auth adapters for login; logout is handled via the session strategy |
Session Auth Levels
Session can contain an optional authLevel value:
partial: primary login succeeded, but MFA steps are still requiredsetup_required: MFA enrollment is required before full authenticationfull: fully authenticated
This package currently enforces route behavior for partial when mfa config is present.
RbacRoutes
type RbacRoutes<ValidRoute, AuthorizedRoles extends string[]> = {
[key in AuthorizedRoles[number]]: {
routes: ValidRoute[];
landing?: ValidRoute;
};
};How It Works
Middleware Protection: The
createMiddlewarehandler checks each request and determines whether the user can access the route or must be redirected.Session Strategy: You provide a session strategy (cookie, token, or custom) to define how sessions are fetched and updated.
Role-Based Access: Routes are protected based on the roles defined in
rbacRoutes. Users can only access routes that match their assigned roles.
TypeScript Support
This library is built with TypeScript and provides full type safety:
// Define your specific types
type MyRoutes = '/home' | '/dashboard' | '/admin';
type MyRoles = 'user' | 'admin' | 'moderator';
// Get full type checking and autocomplete
const config: ReactAuthConfig<MyRoutes, MyRoles[]> = {
// TypeScript will validate all routes and roles
};Generic Action Return Types
You can type adapter action results by passing generics to createPasswordAdapter.
import { createPasswordAdapter } from '@freshheads/react-auth/adapters';
type LoginResponse = {
token: string;
userId: string;
};
type ResetRequestResponse = {
queued: boolean;
};
type ResetResponse = {
success: true;
};
type ChangeResponse = {
success: true;
};
const passwordAdapter = createPasswordAdapter<
LoginResponse,
ResetRequestResponse,
ResetResponse,
ChangeResponse
>({
login: { apiPath: '/api/auth/login' },
resetRequest: { apiPath: '/api/auth/password/reset-request' },
reset: { apiPath: '/api/auth/password/reset' },
change: { apiPath: '/api/auth/password/change' },
});
async function example() {
const loginResult = await passwordAdapter.actions.login({
email: '[email protected]', password: 'secret',
});
// Fully typed as LoginResponse
console.log(loginResult.token);
}If an optional endpoint is not configured (resetRequest, reset, or change),
the return type includes a typed error object:
{ status: 500, error: '...-not-configured' }Usage
1. Define Your Configuration
Create a configuration object that defines your routes, roles, session strategy, and auth adapters:
// src/auth/react-auth.config.ts
import { fetchMutator } from '@/api/fetch';
import { getAuthMeGetUrl, getLogoutPostUrl } from '@/api/generated/auth/auth';
import { MeJsonldRolesItem } from '@/api/generated/model/meJsonldRolesItem';
import { getPasswordLoginPostUrl } from '@/api/generated/password/password';
import { BaseUser, ReactAuthConfig, createAuth } from '@freshheads/react-auth';
import { createPasswordAdapter } from '@freshheads/react-auth/adapters';
import { createAuthHooks } from '@freshheads/react-auth/react';
import { createCookieSessionStrategy } from '@freshheads/react-auth/session';
import { Route } from 'next';
export const authorizedRoles = [
MeJsonldRolesItem.ROLE_MANAGER,
MeJsonldRolesItem.ROLE_COMPANY_ADMIN,
MeJsonldRolesItem.ROLE_USER,
];
type AppRole = (typeof authorizedRoles)[number];
type AppUser = BaseUser<AppRole> & {
language: string; // extend this if there is extra info in your session user.
};
const session = createCookieSessionStrategy<AppUser>({
logoutApiPath: getLogoutPostUrl(),
// Optional: used for client-side refresh when no cookie is available.
// Responses like 401/403 are treated as unauthenticated and do not throw.
sessionApiPath: getAuthMeGetUrl(),
});
const passwordAdapter = createPasswordAdapter({
login: {
apiPath: getPasswordLoginPostUrl(),
},
});
const authConfig: ReactAuthConfig<Route, AppRole[], typeof session> = {
rbacRoutes: { // possibly keep these in a separate file to keep the config clean
ROLE_COMPANY_ADMIN: {
routes: [
'/componentCollection',
'/training-plans/:id/edit',
'/training-plans/new',
'/tags',
'/all-trainings',
'/trainings/:id/preview',
],
},
ROLE_MANAGER: {
routes: [
'/groups',
'/groups/:id',
'/groups/:id/edit',
'/groups/new',
'/employees',
'/employees/:id',
'/employees/:id/edit',
'/employees/new',
'/training-plans',
'/training-plans/:id',
'/training-plans/:id/training/:id',
],
},
ROLE_USER: {
routes: [
'/',
'/trainings/:id',
'/trainings/:id/results/:attemptId',
'/results',
],
},
},
defaultRoutes: {
login: '/login',
landing: '/',
},
openRoutes: [],
unauthenticatedRoutes: ['/login', '/activate-account/:token'],
hiddenRoutePrefix: '{/:locale}', // optional if you are using next-intl
mfa: {
landingRoute: '/mfa',
allowedRoutes: ['/mfa/help'],
},
session: session,
fetchMutator: fetchMutator, // optional
adapters: [passwordAdapter],
};
export const auth = createAuth(authConfig);
// Bind the React hooks to your concrete `auth` instance once, then import
// these project-typed hooks throughout your app. Because `TAuth` is inferred
// from `auth`, they return your real types — `useUser()`/`useSession()` carry
// your custom `AppUser` fields, `usePasswordLogin()` returns your configured
// adapter's result types, and `useAuthUtils().getRolesFromPath()` returns your
// literal route/role types — all with no per-call generics or casts.
export const {
useUser,
useSession,
useAuthUtils,
useAdapterActions,
usePasswordLogin,
} = createAuthHooks(auth);2. Set Up Next.js Middleware
Create or update your middleware.ts or proxy.ts file:
// src/proxy.ts
import { createMiddleware } from '@freshheads/react-auth/next';
import { auth } from './auth';
export default createMiddleware(auth);
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};An example that passes a possible response to another middleware
// src/proxy.ts
import { auth } from '@/auth/react-auth.config';
import { routing } from '@/i18n/routing';
import { createMiddleware as createAuthMiddleware } from '@freshheads/react-auth/next';
import createMiddleware from 'next-intl/middleware';
import { NextRequest } from 'next/server';
export async function proxy(request: NextRequest) {
const handleI18nRouting = createMiddleware(routing);
const I18nResponse = handleI18nRouting(request);
const middleware = createAuthMiddleware(auth);
const response = await middleware(request, I18nResponse);
return response;
}
export const config = {
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
};3. Wrap Your App with AuthProvider
In your root layout or app component:
// src/app/layout.tsx
import Providers from '@/app/[locale]/providers';
import { auth } from '@/auth/react-auth.config';
export default async function RootLayout({
children,
}: LayoutProps<'/[locale]'>) {
const session = await auth.server.getSession();
return (
<html>
<body>
<Providers session={session}>{children}</Providers>
</body>
</html>
);
}// src/app/providers.tsx
import { AuthProvider } from '@freshheads/react-auth/react';
import { auth } from '@/auth/react-auth.config';
export default function Providers({
children,
session,
}: {
children: ReactNode;
session: Session;
}) {
return (
<AuthProvider initialSession={session} auth={auth}>
{children}
</AuthProvider>
);
}5. Use Authentication Hooks
Import the hooks from your own auth module (the createAuthHooks(auth) exports
from step 1), not from @freshheads/react-auth/react. That way every hook is
bound to your project's concrete types.
Deprecated: the plain
useUser,useSession,useAuthUtils,useAdapterActions, andusePasswordLoginexports from@freshheads/react-auth/reactstill work but are@deprecated— they fall back to the genericBaseUser/stringtypes. Prefer thecreateAuthHooksexports. (useAuthContextstays a separate, generic-per-call escape hatch and is not part of the factory.)
Check User Authentication and Roles
'use client';
import { useSession, useUser } from '@/auth/react-auth.config';
export default function Dashboard() {
const { isAuthenticated, authLevel } = useSession();
const { user } = useUser();
return (
<div>
<h1>Dashboard</h1>
<div>Authenticated: {String(isAuthenticated)}</div>
<div>Auth level: {authLevel ?? 'none'}</div>
<div>User: {user?.id ?? 'none'}</div>
{/* `user` is typed as your AppUser, so custom fields are available: */}
<div>Language: {user?.language ?? 'none'}</div>
<pre>{JSON.stringify(user)}</pre>
</div>
);
}Username/password login
When you use the cookie session strategy (createCookieSessionStrategy),
session context is resynced automatically after login — you no longer need to
call refreshSession() yourself. See
Automatic session resync below.
'use client';
import { usePasswordLogin } from '@/auth/react-auth.config';
export default function LoginForm() {
const { login } = usePasswordLogin();
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
const result = await login({
credentials: {
email: '[email protected]',
password: 'pass',
},
});
// No manual refreshSession() needed: because `login` rotates the auth
// cookie, session context is resynced automatically.
router.refresh();
};
return (
<div>
<form onSubmit={handleSubmit}>{/* form fields */}</form>
</div>
);
}Automatic session resync (cookie strategy)
With the cookie session strategy, React's session context (useSession()) and
the auth cookie are two sources of truth. Any request that rotates the cookie as
a side effect — a login, a profile update, or anything else — would otherwise
leave the context stale until you manually called refreshSession().
When the configured session is cookie-backed, the library closes this gap
automatically. For every adapter action invoked through the React hooks
(useAdapterActions, usePasswordLogin, …), it reads the session cookie
immediately before and after the underlying request; if the value changed, it
re-runs getSession() and pushes the fresh session into context for you. This
is generic — any current or future adapter action that rotates the cookie is
covered, with nothing to declare per action.
Notes:
- Non-cookie strategies are unaffected. If your
SessionStrategyis not cookie-backed, this is a complete no-op and you keep managing session state manually, exactly as before. refreshSession()still exists for cases you want to trigger a resync explicitly (e.g. after an idle, purely server-side cookie rotation with no client request).- Resync failures never mask your action. If the follow-up
getSession()fails, the error is logged viaconsole.errorand your original action's result is returned unchanged. - Out of scope: cookie rotation with no client-triggered request passing
through
fetchMutator(e.g. purely server-side or idle rotation) is not detected automatically — resync there still needs an explicitrefreshSession().
logout
'use client';
import { useSession } from '@/auth/react-auth.config';
import { useRouter } from '@/i18n/navigation';
import { useQueryClient } from '@tanstack/react-query';
import { FC } from 'react';
const LogoutButton: FC = () => {
const { logout } = useSession();
const router = useRouter();
const queryClient = useQueryClient();
const handleLogout = async () => {
try {
await logout();
router.refresh();
queryClient.clear();
} catch (error) {
console.error(error);
}
};
return <button onClick={handleLogout}>Uitloggen</button>;
};
export default LogoutButton;
Middleware And Server Header Handling
createMiddleware(auth)passesrequest.headerstoauth.resolveRoute(...).auth.resolveRoute(...)uses the passed headers for session resolution, which is safe in middleware contexts.- Next.js server-only APIs are only used by
auth.server.*helpers.
6. Server-Side Helpers
Require a Session
import { auth } from './auth';
export async function GET() {
const result = await auth.server.requireSession();
if (!result.allowed) {
return Response.redirect(new URL(result.redirect, 'https://example.com'));
}
return Response.json({ user: result.session.user });
}Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
