npm package: @kenac/nextjs-oauth2-proxy-auth
Description: A comprehensive authentication library for Next.js applications using OAuth2 Proxy with full RBAC support, Keycloak integration, and federated logout
Author: Kenac
Latest version: 0.1.0

# @kenac/nextjs-oauth2-proxy-auth

A comprehensive authentication library for Next.js applications using [OAuth2 Proxy](https://oauth2-proxy.github.io/oauth2-proxy/) with full RBAC (Role-Based Access Control) support.

## Features

- 🔐 **OAuth2 Proxy Integration** - Extracts user data from OAuth2 Proxy headers
- 🎭 **Multiple Provider Support** - Keycloak, Auth0, Okta, and generic OAuth2 providers
- 🛡️ **Full RBAC** - Permission and role-based access control
- 🚪 **Federated Logout** - Complete logout flow with OAuth2 provider session clearing
- ⚡ **Server Components** - Full support for Next.js App Router and Server Components
- 🎣 **React Hooks** - Comprehensive hooks for authentication and authorization
- 🧱 **Guard Components** - Client and server-side route protection
- 🔧 **Highly Configurable** - Customize headers, endpoints, and behavior
- 🧪 **Development Mocking** - Built-in mock user for local development

## Installation

```bash
npm install @kenac/nextjs-oauth2-proxy-auth
# or
pnpm add @kenac/nextjs-oauth2-proxy-auth
# or
yarn add @kenac/nextjs-oauth2-proxy-auth
```

## Quick Start (CLI - Recommended)

The fastest way to get started is using our CLI which sets up everything automatically:

```bash
npx @kenac/nextjs-oauth2-proxy-auth init
```

This will:
- ✅ Create `lib/auth.config.ts` with your provider configuration
- ✅ Create `app/api/auth/[...auth]/route.ts` (handles all auth API routes)
- ✅ Create `app/logout-helper/page.tsx` for federated logout
- ✅ Create `app/providers.tsx` with the UserProvider wrapper
- ✅ Create `.env.example` with required environment variables

After running the CLI, just:

1. **Copy environment variables**: `cp .env.example .env.local` and fill in your values
2. **Update `app/layout.tsx`** to wrap children with the Providers component
3. **Configure OAuth2 Proxy** to skip auth for logout helper: `--skip-auth-routes=^/logout-helper`

That's it! Start using authentication in your components:

```tsx
import { useUser, AuthGuard, PermissionGuard } from "@kenac/nextjs-oauth2-proxy-auth";
```

### CLI Options

```bash
# Interactive setup (prompts for provider)
npx @kenac/nextjs-oauth2-proxy-auth init

# Specify provider directly
npx @kenac/nextjs-oauth2-proxy-auth init --provider keycloak
npx @kenac/nextjs-oauth2-proxy-auth init --provider auth0
npx @kenac/nextjs-oauth2-proxy-auth init --provider okta
npx @kenac/nextjs-oauth2-proxy-auth init --provider generic
```

---

## Manual Setup

If you prefer to set up manually, follow these steps:

### 1. Create Configuration

```typescript
// lib/auth-config.ts
import { createAuthConfig } from "@kenac/nextjs-oauth2-proxy-auth";

export const authConfig = createAuthConfig({
  provider: {
    type: "keycloak",
    domain: process.env.KEYCLOAK_DOMAIN!,
    realm: process.env.KEYCLOAK_REALM!,
  },
  development: {
    enableMocking: process.env.NODE_ENV === "development",
    mockUser: {
      email: "dev@example.com",
      name: "Development User",
      roles: ["admin", "user"],
    },
  },
});
```

### 2. Set Up API Routes

**Option A: Single catch-all route (recommended)**

```typescript
// app/api/auth/[...auth]/route.ts
import { createAuthRouteHandler } from "@kenac/nextjs-oauth2-proxy-auth/server";
import { authConfig } from "@/lib/auth-config";

const handler = createAuthRouteHandler(authConfig);

export const GET = handler;
export const dynamic = "force-dynamic";
```

This single file handles `/api/auth/user`, `/api/auth/id-token`, and `/api/auth/health`.

**Option B: Individual routes**

```typescript
// app/api/auth/user/route.ts
import { createUserHandler } from "@kenac/nextjs-oauth2-proxy-auth/server";
import { authConfig } from "@/lib/auth-config";

export const GET = createUserHandler({
  headers: authConfig.headers,
  development: authConfig.development,
});
```

```typescript
// app/api/auth/id-token/route.ts
import { createIdTokenHandler } from "@kenac/nextjs-oauth2-proxy-auth/server";

export const GET = createIdTokenHandler();
```

### 3. Create Logout Helper Page

```typescript
// app/logout-helper/page.tsx
import { LogoutHelperPage } from "@kenac/nextjs-oauth2-proxy-auth";

export default function Page() {
  return <LogoutHelperPage />;
}
```

> ⚠️ **Important**: Configure OAuth2 Proxy to skip auth for this page:
> `--skip-auth-routes=^/logout-helper`

### 4. Wrap Your App with UserProvider

```tsx
// app/providers.tsx
"use client";

import { UserProvider } from "@kenac/nextjs-oauth2-proxy-auth";
import { authConfig } from "@/lib/auth-config";

export function Providers({ children }: { children: React.ReactNode }) {
  return <UserProvider config={authConfig}>{children}</UserProvider>;
}
```

```tsx
// app/layout.tsx
import { Providers } from "./providers";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
```

### 5. Use in Components

```tsx
"use client";

import { useUser, useAuth, PermissionGuard } from "@kenac/nextjs-oauth2-proxy-auth";

export function Dashboard() {
  const { user, loading, isAuthenticated } = useUser();
  const { logout } = useAuth();

  if (loading) return <div>Loading...</div>;
  if (!isAuthenticated) return <div>Please log in</div>;

  return (
    <div>
      <h1>Welcome, {user.name}!</h1>
      
      <PermissionGuard permission="dashboard:admin" fallback={<p>Admin only</p>}>
        <AdminPanel />
      </PermissionGuard>
      
      <button onClick={() => logout()}>Logout</button>
    </div>
  );
}
```

## Configuration

### Full Configuration Example

```typescript
import { createAuthConfig } from "@kenac/nextjs-oauth2-proxy-auth";

const authConfig = createAuthConfig({
  // OAuth2 Provider Configuration
  provider: {
    type: "keycloak", // "keycloak" | "auth0" | "okta" | "generic"
    domain: "https://auth.example.com",
    realm: "my-realm", // For Keycloak
    clientId: "my-client", // Optional
  },

  // Header Configuration (customize if OAuth2 Proxy uses different headers)
  headers: {
    userIdHeaders: ["x-forwarded-user", "x-user", "x-auth-request-user"],
    emailHeaders: ["x-forwarded-email", "x-email", "x-auth-request-email"],
    usernameHeaders: ["x-forwarded-preferred-username"],
    groupsHeaders: ["x-forwarded-groups", "x-groups"],
  },

  // RBAC Configuration
  rbac: {
    enabled: true,
    fetchPermissionsOnLogin: true,
    permissionsCacheDuration: 5 * 60 * 1000, // 5 minutes
    // Custom permission fetcher
    fetchPermissions: async (userId) => {
      const res = await fetch(`/api/permissions?userId=${userId}`);
      const data = await res.json();
      return data.permissions;
    },
  },

  // Logout Configuration
  logout: {
    oauth2ProxyLogout: "/oauth2/sign_out",
    logoutHelperPath: "/logout-helper",
    postLogoutRedirect: "/",
    includeIdTokenHint: true,
    localStorageKeys: ["user", "auth_token"],
  },

  // Development Configuration
  development: {
    enableMocking: process.env.NODE_ENV === "development",
    enableLogging: true,
    mockUser: {
      id: "dev-user",
      email: "dev@example.com",
      name: "Dev User",
      roles: ["admin"],
      permissions: ["dashboard:view", "users:manage"],
    },
  },

  // Endpoints Configuration
  endpoints: {
    userInfo: "/api/auth/user",
    idToken: "/api/auth/id-token",
    login: "/oauth2/start",
    logout: "/oauth2/sign_out",
  },
});
```

## Hooks

### `useUser()`

Access the current user and authentication state.

```tsx
const { user, loading, error, isAuthenticated, refetch } = useUser();
```

### `useAuth()`

Access authentication actions.

```tsx
const { login, logout, refreshSession, validateSession, clearLocalSession } = useAuth();

// Login with redirect
login("/dashboard");

// Logout
await logout();

// Logout without provider logout (just clear local session)
await logout({ skipProviderLogout: true });
```

### `usePermissions()`

Check user permissions and roles.

```tsx
const {
  hasPermission,
  hasAnyPermission,
  hasAllPermissions,
  hasRole,
  hasAnyRole,
  isAdmin,
  isManager,
  permissions,
  roles,
} = usePermissions();

// Check single permission
if (hasPermission("users:delete")) {
  // ...
}

// Check multiple permissions
if (hasAnyPermission(["users:edit", "users:delete"])) {
  // ...
}
```

### `useUserDisplay()`

Get formatted user display information.

```tsx
const { displayName, displayEmail, initials, avatar, primaryRole } = useUserDisplay();
```

## Components

### `<AuthGuard>`

Protect content based on authentication.

```tsx
<AuthGuard fallback={<LoginPrompt />}>
  <ProtectedContent />
</AuthGuard>

<AuthGuard when="unauthenticated">
  <LoginButton />
</AuthGuard>

<AuthGuard roles={["admin", "manager"]} fallback={<AccessDenied />}>
  <AdminContent />
</AuthGuard>
```

### `<PermissionGuard>`

Protect content based on permissions.

```tsx
<PermissionGuard permission="users:delete" fallback={<NoAccess />}>
  <DeleteButton />
</PermissionGuard>

<PermissionGuard permissions={["users:edit", "users:view"]} requireAll>
  <UserEditor />
</PermissionGuard>
```

### `<RoleGuard>`

Protect content based on roles.

```tsx
<RoleGuard role="admin">
  <AdminPanel />
</RoleGuard>

<RoleGuard roles={["admin", "manager"]}>
  <ManagementTools />
</RoleGuard>
```

### `withAuth()` HOC

Protect entire components with a higher-order component.

```tsx
const ProtectedPage = withAuth(MyPage, {
  requiredRoles: ["admin"],
  redirectUrl: "/login",
  fallback: AccessDeniedComponent,
});
```

## Server-Side Usage

### Server Actions

```typescript
// lib/actions/get-user.ts
"use server";

import { createServerUserAction } from "@kenac/nextjs-oauth2-proxy-auth/server";
import { authConfig } from "@/lib/auth-config";

export const getServerUser = createServerUserAction({
  headers: authConfig.headers,
  development: authConfig.development,
  enrichUserWithProfile: async (user) => {
    // Fetch additional user data from your database
    const profile = await db.profiles.findByEmail(user.email);
    return { ...user, profileId: profile?.id };
  },
  fetchRBAC: async (userId) => {
    // Fetch roles and permissions from your backend
    const rbac = await db.rbac.getForUser(userId);
    return {
      roles: rbac.roles,
      permissions: rbac.permissions,
    };
  },
});
```

### Server Components (Page Guards)

```typescript
// lib/guards.ts
import { createServerGuards } from "@kenac/nextjs-oauth2-proxy-auth/server";
import { getServerUser } from "@/lib/actions/get-user";

export const { PermissionGuard, RoleGuard, AdminGuard, AuthGuard } =
  createServerGuards(getServerUser);
```

```tsx
// app/admin/page.tsx
import { AdminGuard } from "@/lib/guards";

export default async function AdminPage() {
  return (
    <AdminGuard redirectTo="/unauthorized">
      <h1>Admin Dashboard</h1>
    </AdminGuard>
  );
}
```

## OAuth2 Proxy Configuration

Ensure your OAuth2 Proxy is configured to pass the required headers:

```yaml
# oauth2-proxy.cfg or environment variables
pass_user_headers = true
pass_authorization_header = true  # Required for logout with id_token_hint
set_xauthrequest = true

# Skip auth for logout helper page
skip_auth_routes = ["/logout-helper"]
```

## Logout Flow

The library implements a complete federated logout flow:

1. **Frontend** calls `logout()` → fetches ID token, clears local storage
2. **Redirect to OAuth2 Provider** (e.g., Keycloak) with `id_token_hint`
3. **Provider clears session** → redirects to `/logout-helper`
4. **Logout helper** redirects to `/oauth2/sign_out`
5. **OAuth2 Proxy** clears its session → redirects to home

## TypeScript

The library is written in TypeScript and includes full type definitions.

```typescript
import type {
  User,
  UserResponse,
  OAuth2ProxyAuthConfig,
  PermissionCheck,
} from "@kenac/nextjs-oauth2-proxy-auth";
```

## License

MIT

## Contributing

Contributions are welcome! Please read our contributing guidelines before submitting a pull request.