@groo.dev/auth-server
v0.7.2
Published
Server-side authentication middleware for Groo Auth SDK
Readme
@groo.dev/auth-server
Server-side authentication middleware and utilities for Hono and Cloudflare Workers.
Installation
npm install @groo.dev/auth-server honoFeatures
- 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/meendpoint 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
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 appAPI Reference
grooAuth(config)
Creates a core authentication instance for session validation and M2M operations.
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.
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).
app.use('*', hono.init) // Required - enables c.get('groo')hono.middleware
Middleware that requires authentication AND consent. Returns 401 if not authenticated.
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.
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.
// 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:
c.get('groo') // GrooAuth instance for M2M operations
c.get('user') // ConsentedUser (after middleware) or null
c.get('apiToken') // ApiTokenInfo (after apiTokenMiddleware) or nullAPI Token Authentication (M2M)
API tokens allow external services (GitHub Actions, cron jobs, other backends) to authenticate with your API without user sessions.
Flow
- Create an API token in the Groo Accounts dashboard
- Give the token to your external service (e.g., GitHub Actions secret)
- External service sends requests with
Authorization: Bearer groo_xxxx - Your API verifies the token via
apiTokenMiddleware
Example: Webhook Endpoint
// 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.
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
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):
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:
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'))
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('[email protected]')
// 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:
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
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
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
// 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 appCloudflare Workers Configuration
// wrangler.jsonc
{
"vars": {
"ACCOUNTS_URL": "https://accounts.groo.dev"
}
}
// Store secrets securely:
// wrangler secret put CLIENT_ID
// wrangler secret put CLIENT_SECRETHow It Works
Session-Based Authentication
- Frontend sends requests with session cookie
- Middleware validates session AND checks consent via accounts API
- If valid and consented,
ConsentedUser(includingappData) is added to Hono context - Route handlers access user via
c.get('user')
Machine-to-Machine Operations
- Use
c.get('groo')to access the auth client in route handlers - Client uses Basic auth with
clientIdandclientSecret - Can list/get users who have consented to the application
- Can store and retrieve app-specific data for each user
Related Packages
@groo.dev/auth-react- React hooks and components@groo.dev/auth-core- Shared types and utilities
License
MIT
