npm package: @groo.dev/auth-server
Description: Server-side authentication middleware for Groo Auth SDK
Latest version: 0.7.6

# @groo.dev/auth-server

Server-side authentication middleware and utilities for Hono and Cloudflare Workers.

## Installation

```bash
npm install @groo.dev/auth-server hono
```

## Features

- **Unified Authentication** - Single initialization for middleware and M2M operations
- **User Authentication Middleware** - Validate session cookies and protect routes
- **API Token Authentication** - Verify API tokens for machine-to-machine (M2M) requests
- **Token Management** - Programmatically create, list, and revoke API tokens
- **Auth Routes Proxy** - Handle `/__auth/me` endpoint for frontend SDKs
- **Machine-to-Machine Client** - Access consented users via client credentials
- **App Data** - Store and retrieve app-specific data for users and tokens

## Quick Start

```typescript
import { Hono } from 'hono'
import { grooAuth } from '@groo.dev/auth-server'
import { GrooHonoMiddleware } from '@groo.dev/auth-server/hono'

type Env = {
  CLIENT_ID: string
  CLIENT_SECRET: string
}

// Create middleware with factory (env available at request time)
const hono = new GrooHonoMiddleware<Env>((env) => grooAuth({
  clientId: env.CLIENT_ID,
  clientSecret: env.CLIENT_SECRET,
}))

const app = new Hono<{ Bindings: Env }>()

// Initialize groo in context (required - must be first)
app.use('*', hono.init)

// Mount auth routes for frontend SDK (handles /__auth/me)
app.route('/v1', hono.routes)

// Protected route - requires authentication AND consent
app.get('/v1/profile', hono.middleware, (c) => {
  const user = c.get('user')  // ConsentedUser with appData
  return c.json({ user })
})

// M2M operations - use c.get('groo')
app.get('/v1/admin/users', hono.middleware, async (c) => {
  const user = c.get('user')
  if (user?.role !== 'admin') {
    return c.json({ error: 'Forbidden' }, 403)
  }
  const groo = c.get('groo')
  const result = await groo.getUsers()
  return c.json(result)
})

export default app
```

## API Reference

### grooAuth(config)

Creates a core authentication instance for session validation and M2M operations.

```typescript
import { grooAuth } from '@groo.dev/auth-server'

const groo = grooAuth({
  clientId: 'your-client-id',      // Required
  clientSecret: 'your-client-secret',  // Required
  baseUrl: 'https://accounts.groo.dev',  // Optional (default)
  cookieName: 'session',  // Optional (default)
})
```

### GrooHonoMiddleware

Creates Hono-specific middleware with a factory function for Cloudflare Workers environment.

```typescript
import { GrooHonoMiddleware } from '@groo.dev/auth-server/hono'

type Env = { CLIENT_ID: string; CLIENT_SECRET: string }

const hono = new GrooHonoMiddleware<Env>((env) => grooAuth({
  clientId: env.CLIENT_ID,
  clientSecret: env.CLIENT_SECRET,
}))
```

#### hono.init

Middleware that initializes `groo` in context. **Must be called first** with `app.use('*', hono.init)`.

```typescript
app.use('*', hono.init)  // Required - enables c.get('groo')
```

#### hono.middleware

Middleware that requires authentication AND consent. Returns 401 if not authenticated.

```typescript
app.get('/api/protected', hono.middleware, (c) => {
  const user = c.get('user')  // ConsentedUser with consent.appData
  return c.json({ user })
})
```

#### hono.optionalMiddleware

Middleware that adds user to context but doesn't require authentication.

```typescript
app.get('/api/optional', hono.optionalMiddleware, (c) => {
  const user = c.get('user')  // ConsentedUser or null
  return c.json({ authenticated: !!user })
})
```

#### hono.routes

A Hono router that handles authentication routes for frontend SDKs. Returns 401 if not authenticated.

```typescript
// Mount at /v1 to handle /v1/__auth/me (requires authentication)
app.route('/v1', hono.routes)
```

### Context Variables

After `hono.init` is called, these are available in all routes:

```typescript
c.get('groo')      // GrooAuth instance for M2M operations
c.get('user')      // ConsentedUser (after middleware) or null
c.get('apiToken')  // ApiTokenInfo (after apiTokenMiddleware) or null
```

### API Token Authentication (M2M)

API tokens allow external services (GitHub Actions, cron jobs, other backends) to authenticate with your API without user sessions.

#### Flow

1. Create an API token in the Groo Accounts dashboard
2. Give the token to your external service (e.g., GitHub Actions secret)
3. External service sends requests with `Authorization: Bearer groo_xxxx`
4. Your API verifies the token via `apiTokenMiddleware`

#### Example: Webhook Endpoint

```typescript
// Protected by API token (not user session)
app.post('/v1/webhook', hono.apiTokenMiddleware, (c) => {
  const token = c.get('apiToken')
  console.log(`Request from: ${token.application_name} / ${token.token_name}`)
  return c.json({ received: true })
})

// GitHub Action can call this with:
// curl -X POST https://api.myapp.com/v1/webhook \
//   -H "Authorization: Bearer groo_xxxx"
```

#### hono.apiTokenMiddleware

Middleware that requires a valid API token. Returns 401 if token is missing or invalid.

```typescript
app.post('/v1/internal/sync', hono.apiTokenMiddleware, (c) => {
  const token = c.get('apiToken')  // ApiTokenInfo
  return c.json({
    application: token.application_name,
    token: token.token_name,
  })
})
```

#### ApiTokenInfo Object

```typescript
interface ApiTokenInfo {
  active: true
  client_id: string
  token_type: 'Bearer'
  exp?: number              // Expiration timestamp (if set)
  iat: number               // Issued at timestamp
  sub: string               // Token ID
  aud: string               // Client ID
  application_id: string
  application_name: string
  token_name: string
  token_description: string | null
  app_data: Record<string, unknown>  // Token-specific custom data
}

interface ApiToken<T = Record<string, unknown>> {
  id: string
  applicationId: string
  name: string
  description: string | null
  tokenPrefix: string        // Last 4 chars (e.g., "a1b2")
  createdBy: string | null   // User ID if created from dashboard, null if M2M
  lastUsed: string | null
  expiresAt: string | null
  expired: boolean
  revoked: boolean
  appData: T
  createdAt: string
}
```

#### Using Token App Data

Each token can store custom metadata (e.g., rate limits, permissions, environment):

```typescript
app.post('/v1/webhook', hono.apiTokenMiddleware, (c) => {
  const token = c.get('apiToken')

  // Access token-specific configuration
  const rateLimit = token.app_data.rate_limit as number || 1000
  const environment = token.app_data.environment as string || 'production'

  console.log(`Token ${token.token_name}: rate_limit=${rateLimit}, env=${environment}`)

  return c.json({ received: true })
})
```

#### Direct Token Verification

You can also verify tokens manually without middleware:

```typescript
app.post('/v1/custom', async (c) => {
  const groo = c.get('groo')
  const authHeader = c.req.header('Authorization')

  if (authHeader?.startsWith('Bearer ')) {
    const token = authHeader.slice(7)
    const tokenInfo = await groo.verifyApiToken(token)

    if (tokenInfo) {
      // Token is valid
      return c.json({ valid: true, app: tokenInfo.application_name })
    }
  }

  return c.json({ error: 'Unauthorized' }, 401)
})
```

### M2M Operations (via c.get('groo'))

```typescript
app.get('/v1/admin/users', hono.middleware, async (c) => {
  const groo = c.get('groo')

  // List all consented users
  const { users, total } = await groo.getUsers({ page: 1, perPage: 20 })

  // Get specific user by ID
  const user = await groo.getUser('user-id')

  // Get user by email
  const userByEmail = await groo.getUserByEmail('user@example.com')

  // Get user by phone
  const userByPhone = await groo.getUserByPhone('+1234567890')

  // Get/set app data
  const data = await groo.getUserData('user-id')
  await groo.setUserData('user-id', { plan: 'premium' })
})
```

### Token Management

Programmatically manage API tokens for your application. All token methods support generic types for type-safe `appData`:

```typescript
const groo = grooAuth({
  clientId: env.CLIENT_ID,
  clientSecret: env.CLIENT_SECRET,
})

// Define your token data type
interface TokenData {
  environment: 'production' | 'staging'
  permissions: string[]
}

// List all tokens (with typed appData)
const tokens = await groo.getTokens<TokenData>()

// Create a new token (with typed appData)
const { token, secret } = await groo.createToken<TokenData>({
  name: 'CI/CD Pipeline',
  description: 'For automated deployments',
  expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days
  appData: { environment: 'production', permissions: ['deploy'] },
})
console.log('Save this secret:', secret) // Only shown once!

// Get a specific token (with typed appData)
const tokenInfo = await groo.getToken<TokenData>(token.id)

// Update token app data (type-safe)
await groo.setTokenData<TokenData>(token.id, {
  environment: 'staging',
  permissions: ['deploy', 'rollback'],
})

// Get token app data (typed)
const data = await groo.getTokenData<TokenData>(token.id)
// data.environment is typed as 'production' | 'staging'

// Revoke a token
await groo.revokeToken(token.id)
```

#### Token Management Methods

| Method | Description |
|--------|-------------|
| `getTokens()` | List all API tokens for the application |
| `getToken(tokenId)` | Get a specific token by ID |
| `createToken(options)` | Create a new API token (returns secret once!) |
| `revokeToken(tokenId)` | Revoke/delete a token |
| `getTokenData(tokenId)` | Get token-specific app data |
| `setTokenData(tokenId, data)` | Set token-specific app data |

#### CreateTokenOptions

```typescript
interface CreateTokenOptions {
  name: string              // Required: token name
  description?: string      // Optional: description
  expiresAt?: string | Date // Optional: expiration date
  appData?: Record<string, unknown> // Optional: custom data
}
```

## User Object

```typescript
interface User {
  id: string
  email: string | null
  phone: string | null
  name: string | null
  role: string
}

interface ConsentedUser extends User {
  consent: {
    id: string
    userId: string
    applicationId: string
    consentedAt: string
    lastAccessedAt: string
    revokedAt: string | null
    appData: Record<string, unknown>  // App-specific user data
  }
}
```

## Full Example

```typescript
// src/index.ts
import { Hono } from 'hono'
import { grooAuth } from '@groo.dev/auth-server'
import { GrooHonoMiddleware } from '@groo.dev/auth-server/hono'

type Env = {
  CLIENT_ID: string
  CLIENT_SECRET: string
  ACCOUNTS_URL: string
}

const hono = new GrooHonoMiddleware<Env>((env) => grooAuth({
  clientId: env.CLIENT_ID,
  clientSecret: env.CLIENT_SECRET,
  baseUrl: env.ACCOUNTS_URL,
}))

const app = new Hono<{ Bindings: Env }>()

// Initialize groo in context
app.use('*', hono.init)

// Mount auth routes for frontend SDK
app.route('/v1', hono.routes)

// Public endpoint
app.get('/v1/health', (c) => {
  return c.json({ status: 'ok' })
})

// Protected endpoint
app.get('/v1/profile', hono.middleware, (c) => {
  const user = c.get('user')
  return c.json({ user })
})

// Admin endpoint - list all consented users
app.get('/v1/admin/users', hono.middleware, async (c) => {
  const user = c.get('user')
  if (user?.role !== 'admin' && user?.role !== 'superadmin') {
    return c.json({ error: 'Forbidden' }, 403)
  }

  const groo = c.get('groo')
  const result = await groo.getUsers()
  return c.json(result)
})

export default app
```

## Cloudflare Workers Configuration

```jsonc
// wrangler.jsonc
{
  "vars": {
    "ACCOUNTS_URL": "https://accounts.groo.dev"
  }
}

// Store secrets securely:
// wrangler secret put CLIENT_ID
// wrangler secret put CLIENT_SECRET
```

## How It Works

### Session-Based Authentication
1. Frontend sends requests with session cookie
2. Middleware validates session AND checks consent via accounts API
3. If valid and consented, `ConsentedUser` (including `appData`) is added to Hono context
4. Route handlers access user via `c.get('user')`

### Machine-to-Machine Operations
1. Use `c.get('groo')` to access the auth client in route handlers
2. Client uses Basic auth with `clientId` and `clientSecret`
3. Can list/get users who have consented to the application
4. Can store and retrieve app-specific data for each user

## Related Packages

- [`@groo.dev/auth-react`](https://www.npmjs.com/package/@groo.dev/auth-react) - React hooks and components
- [`@groo.dev/auth-core`](https://www.npmjs.com/package/@groo.dev/auth-core) - Shared types and utilities

## License

MIT