npm package: @sp-uvb/next
Description: Next.js middleware for Universal Verification Broker (UVB)
Author: UVB Team
Latest version: 0.1.0

# @sp-uvb/next

Next.js middleware for Universal Verification Broker (UVB) authentication. Supports both App Router and Pages Router.

## Installation

```bash
npm install @sp-uvb/next
# or
yarn add @sp-uvb/next
# or
pnpm add @sp-uvb/next
```

## Quick Start

### App Router (Next.js 13+)

Create `middleware.ts` in your project root:

```typescript
import { createUvbMiddleware } from '@sp-uvb/next';

export default createUvbMiddleware({
  tenantId: 'my-tenant',
  uvbUrl: 'http://localhost:8080',
  excludePaths: ['/login', '/api/auth'],
  loginUrl: '/login', // Optional: redirect to login page
});

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
```

Access session in Server Components:

```typescript
import { getUvbSessionServer } from '@sp-uvb/next/server';

export default async function ProfilePage() {
  const session = await getUvbSessionServer();

  return (
    <div>
      <h1>User Profile</h1>
      <p>User ID: {session?.userId}</p>
      <p>Factors: {session?.factorsVerified.join(', ')}</p>
    </div>
  );
}
```

### Pages Router (Next.js 12+)

Protect API routes:

```typescript
// pages/api/profile.ts
import { requireUvbAuth, getUvbSession } from '@sp-uvb/next/server';

export default requireUvbAuth(async function handler(req, res) {
  const session = getUvbSession(req);
  res.json({
    userId: session.userId,
    factors: session.factorsVerified,
  });
});
```

## API

### App Router

#### `createUvbMiddleware(config)`

Create Next.js middleware for App Router.

**Configuration:**

- `tenantId` (required): Your UVB tenant ID
- `uvbUrl` (optional): UVB server URL, defaults to `http://localhost:8080`
- `apiKey` (optional): API key for server-to-server authentication
- `cookieName` (optional): Cookie name for session token, defaults to `uvb_session`
- `excludePaths` (optional): Array of paths to exclude from authentication
- `loginUrl` (optional): Redirect URL for unauthenticated users

```typescript
// middleware.ts
import { createUvbMiddleware } from '@sp-uvb/next';

export default createUvbMiddleware({
  tenantId: 'my-tenant',
  uvbUrl: process.env.UVB_URL,
  loginUrl: '/login',
});
```

#### `getUvbSessionServer()`

Get session in App Router Server Components.

```typescript
import { getUvbSessionServer } from '@sp-uvb/next/server';

export default async function Page() {
  const session = await getUvbSessionServer();
  if (!session) {
    return <div>Not authenticated</div>;
  }
  return <div>Hello {session.userId}</div>;
}
```

#### `requireFactors(factors)`

Middleware to require specific MFA factors in App Router.

```typescript
// middleware.ts
import { createUvbMiddleware, requireFactors } from '@sp-uvb/next';
import { NextResponse } from 'next/server';

const uvbMiddleware = createUvbMiddleware({
  tenantId: 'my-tenant',
});

const adminFactors = requireFactors(['totp', 'webauthn']);

export async function middleware(request) {
  // Apply base auth
  const response = await uvbMiddleware(request);
  if (response.status !== 200) return response;

  // Require additional factors for /admin
  if (request.nextUrl.pathname.startsWith('/admin')) {
    return adminFactors(request);
  }

  return NextResponse.next();
}
```

### Pages Router / Server-Side

#### `getUvbSession(req)`

Get session from API route or getServerSideProps.

```typescript
import { getUvbSession } from '@sp-uvb/next/server';

// In API route
export default function handler(req, res) {
  const session = getUvbSession(req);
  if (!session) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  res.json({ userId: session.userId });
}

// In getServerSideProps
export async function getServerSideProps({ req }) {
  const session = getUvbSession(req);
  if (!session) {
    return { redirect: { destination: '/login', permanent: false } };
  }
  return { props: { session } };
}
```

#### `requireUvbAuth(handler)`

HOF to require authentication for API routes.

```typescript
import { requireUvbAuth } from '@sp-uvb/next/server';

export default requireUvbAuth(async function handler(req, res) {
  // User is authenticated
  res.json({ message: 'Protected data' });
});
```

#### `requireFactorsApi(factors)`

HOF to require specific MFA factors for API routes.

```typescript
import { requireFactorsApi } from '@sp-uvb/next/server';

export default requireFactorsApi(['totp', 'webauthn'])(async function handler(req, res) {
  res.json({ message: 'Admin action completed' });
});
```

#### `validateUvbSession(token, tenantId, uvbUrl?, apiKey?)`

Validate a session token directly (for custom flows).

```typescript
import { validateUvbSession } from '@sp-uvb/next/server';

export default async function handler(req, res) {
  const token = req.cookies.uvb_session;
  const session = await validateUvbSession(token, 'my-tenant', 'http://localhost:8080');

  if (!session) {
    return res.status(401).json({ error: 'Invalid session' });
  }

  res.json({ userId: session.userId });
}
```

## Examples

### Protecting Specific Routes (App Router)

```typescript
// middleware.ts
import { createUvbMiddleware } from '@sp-uvb/next';

export default createUvbMiddleware({
  tenantId: 'my-tenant',
  excludePaths: ['/login', '/register', '/api/auth', '/public'],
});

export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*', '/profile/:path*'],
};
```

### Conditional MFA Requirements

```typescript
// middleware.ts
import { createUvbMiddleware, requireFactors } from '@sp-uvb/next';
import { NextResponse } from 'next/server';

const baseAuth = createUvbMiddleware({ tenantId: 'my-tenant' });
const strictAuth = requireFactors(['totp', 'webauthn']);

export async function middleware(request) {
  const response = await baseAuth(request);
  if (response.status !== 200) return response;

  // Require strong auth for sensitive routes
  if (
    request.nextUrl.pathname.startsWith('/admin') ||
    request.nextUrl.pathname.startsWith('/transfer')
  ) {
    return strictAuth(request);
  }

  return NextResponse.next();
}
```

### Server Actions (App Router)

```typescript
// app/actions.ts
'use server';

import { getUvbSessionServer } from '@sp-uvb/next/server';

export async function updateProfile(formData: FormData) {
  const session = await getUvbSessionServer();

  if (!session) {
    throw new Error('Not authenticated');
  }

  // Update user profile using session.userId
  // ...
}
```

### API Route with Factor Requirements (Pages Router)

```typescript
// pages/api/admin/delete.ts
import { requireFactorsApi, getUvbSession } from '@sp-uvb/next/server';

export default requireFactorsApi(['totp', 'webauthn'])(async function handler(req, res) {
  const session = getUvbSession(req);

  // Perform admin action
  res.json({ message: `Admin ${session.userId} performed action` });
});
```

### Custom Authentication Flow

```typescript
// pages/api/custom-auth.ts
import { validateUvbSession } from '@sp-uvb/next/server';

export default async function handler(req, res) {
  const customToken = req.headers['x-custom-token'];

  if (!customToken) {
    return res.status(401).json({ error: 'No token' });
  }

  const session = await validateUvbSession(customToken, 'my-tenant', process.env.UVB_URL);

  if (!session) {
    return res.status(401).json({ error: 'Invalid token' });
  }

  res.json({ userId: session.userId });
}
```

### Redirect After Login

```typescript
// middleware.ts
import { createUvbMiddleware } from '@sp-uvb/next';

export default createUvbMiddleware({
  tenantId: 'my-tenant',
  loginUrl: '/login' // Redirects to /login?from=/original-path
});

// pages/login.tsx
import { useRouter } from 'next/router';

export default function LoginPage() {
  const router = useRouter();
  const { from } = router.query;

  const handleLogin = async () => {
    // Perform login
    // ...

    // Redirect back to original page
    router.push((from as string) || '/dashboard');
  };

  return <button onClick={handleLogin}>Login</button>;
}
```

### Server Component with Auth Check

```typescript
// app/profile/page.tsx
import { getUvbSessionServer } from '@sp-uvb/next/server';
import { redirect } from 'next/navigation';

export default async function ProfilePage() {
  const session = await getUvbSessionServer();

  if (!session) {
    redirect('/login');
  }

  // Check for specific factors
  const hasStrongAuth = session.factorsVerified.includes('totp') &&
                        session.factorsVerified.includes('webauthn');

  return (
    <div>
      <h1>Profile</h1>
      <p>User ID: {session.userId}</p>
      <p>Strong Auth: {hasStrongAuth ? 'Yes' : 'No'}</p>
    </div>
  );
}
```

## TypeScript

This package includes full TypeScript definitions:

```typescript
import type { UvbSession, UvbMiddlewareConfig } from '@sp-uvb/next';
```

## License

MIT