@freshheads/react-auth
v0.0.5
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 { 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);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
Check User Authentication and Roles
'use client';
import { useSession, useUser } from '@freshheads/react-auth/react';
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>
<pre>{JSON.stringify(user)}</pre>
</div>
);
}
#### Username/password login
```typescript
'use client';
import { useSession, usePasswordLogin } from '@freshheads/react-auth/react';
export default function LoginForm() {
const { login } = usePasswordLogin();
const { refreshSession } = useSession();
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
const result = await login({
credentials: {
email: '[email protected]',
password: 'pass',
},
});
await refreshSession();
router.refresh();
};
return (
<div>
<form onSubmit={handleSubmit}>{/* form fields */}</form>
</div>
);
}logout
'use client';
import { useSession } from '@freshheads/react-auth/react';
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.
