@splendidlabz/auth
v1.1.2
Published
A comprehensive authentication system built specifically for Astro applications. This package provides both server-side and client-side authentication components with support for database sessions or stateless cookie-based authentication.
Readme
Auth Package
A comprehensive authentication system built specifically for Astro applications. This package provides both server-side and client-side authentication components with support for database sessions or stateless cookie-based authentication.
Installation
npm install @splendidlabz/authFor Drizzle ORM support, also install:
npm install drizzle-orm
# Choose your database driver:
npm install better-sqlite3 # For SQLite
npm install pg # For PostgreSQL
npm install mysql2 # For MySQLBasic Usage
This auth package supports two authentication strategies and two database options:
Authentication Strategies
1. Cookie Strategy (Stateless)
Uses signed cookies to store session data. No database sessions required.
// src/services/auth.js
import { auth as splAuth } from '@splendidlabz/auth'
import { db, User } from 'astro:db'
export const auth = splAuth({
db, // Still needed for user lookup
tables: { User }, // Only User table required
auth: {
strategy: 'cookie',
secret: process.env.AUTH_SECRET, // Required for cookie signing
userAttributes: ['email', 'first_name', 'name'],
cookieOptions: {
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
},
},
})2. Database Strategy (Stateful)
Stores sessions in the database for centralized session management.
// src/services/auth.js
import { auth as splAuth } from '@splendidlabz/auth'
import { db, User, Session, PasswordResetToken } from 'astro:db'
export const auth = splAuth({
db,
tables: { User, Session, PasswordResetToken },
auth: {
strategy: 'db',
userAttributes: ['email', 'first_name', 'name'],
},
})Database Options
Option 1: Using Astro DB (Recommended)
// src/services/auth.js
import { auth as splAuth } from '@splendidlabz/auth'
import { db, User, Session, PasswordResetToken } from 'astro:db'
export const auth = splAuth({
db,
tables: { User, Session, PasswordResetToken },
auth: {
strategy: 'db', // or 'cookie'
userAttributes: ['email', 'first_name', 'name'],
},
})Option 2: Using Drizzle ORM
// src/services/auth.js
import { auth as splAuth } from '@splendidlabz/auth'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import { users, sessions, passwordResetTokens } from './db/schema.js'
const sqlite = new Database('auth.db')
const db = drizzle(sqlite)
export const auth = splAuth({
db,
tables: {
User: users,
Session: sessions, // Only for 'db' strategy
PasswordResetToken: passwordResetTokens,
},
auth: {
strategy: 'db', // or 'cookie'
userAttributes: ['email', 'firstName', 'name'],
},
})Web/Client-side Setup
// src/services/web/auth.js
import { webAuth as wa } from '@splendidlabz/auth/web'
export const webAuth = wa({
tokenName: 'auth_token',
authEndpoint: '/api/auth/authenticate/',
})Basic Implementation
1. Login Page
---
// src/pages/auth/login.astro
import { auth } from '@/services/auth.js'
import { parseData } from '@splendidlabz/astro/server'
const redirectTo = Astro.url.searchParams.get('redirect_to')
export const prerender = false
let error
if (Astro.request.method === 'POST') {
const body = await parseData(Astro)
const { email, password } = body
try {
await auth.login(Astro, { email, password })
return Astro.redirect(redirectTo || '/dashboard/')
} catch (e) {
error = e
}
}
---
<h1>Login</h1>
<form method="POST">
<input type="email" name="email" placeholder="Email" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit">Log in</button>
</form>
{error && <p class="error">{error.message}</p>}2. Middleware Protection
// src/middleware/index.js
import { auth } from '@/services/auth.js'
import { AuthManager } from '@splendidlabz/utils'
import { sequence } from 'astro:middleware'
export const onRequest = sequence(authenticate)
const { isProtectedRoute } = AuthManager({
protectedRoutes: {
is: ['/dashboard/'],
startsWith: ['/account'],
},
})
async function authenticate(context, next) {
if (!isProtectedRoute(context.url.pathname)) return next()
try {
await auth.authenticate(context)
return next()
} catch (e) {
const originalPathname = context.url.pathname
return context.redirect(`/auth/login/?redirect_to=${originalPathname}`)
}
}3. API Routes
// src/pages/api/auth/authenticate.js
import { auth } from '@/services/auth.js'
export async function POST(context) {
try {
const result = await auth.authenticate(context)
return new Response(JSON.stringify(result), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
})
}
}Config
Strategy Comparison
| Feature | Database Strategy | Cookie Strategy | | ------------------------ | ---------------------------------------- | --------------------------- | | Session Storage | Database tables | Signed cookies | | State | Stateful | Stateless | | Database Requirement | User, Session, PasswordResetToken tables | Only User table | | Server Memory | Low | Very low | | Security | Session invalidation possible | Relies on cookie expiration | | Logout | Immediate (removes from DB) | Client-side only | | Session Management | Centralized | Distributed | | Best For | Multi-device apps, admin panels | Simple apps, APIs |
Use Database Strategy when:
- You need immediate session invalidation
- Building multi-device applications
- Want centralized session management
- Need to track active sessions
Use Cookie Strategy when:
- Building stateless applications
- Want minimal database overhead
- Working with microservices/APIs
- Don't need session tracking
Configuration Options
Server Auth Configuration
export const auth = splAuth({
db, // Database instance (required)
tables: {}, // Database tables (required)
auth: {
strategy, // 'db' or 'cookie' (required)
secret, // Required for 'cookie' strategy
userAttributes, // Array of user fields to include in session
cookieOptions, // Cookie settings (cookie strategy only)
additionalVerification, // Custom verification function
},
})Parameters:
- db: Your database instance (Astro DB or Drizzle)
- tables: Object mapping table names to table schemas
- auth.strategy:
'db'for database sessions,'cookie'for stateless - auth.secret: Secret key for cookie signing (required for cookie strategy)
- auth.userAttributes: Array of user fields to include in session data
- auth.cookieOptions: Cookie configuration object (cookie strategy only)
- auth.additionalVerification: Optional async function for custom verification
Cookie Options (Cookie Strategy Only)
cookieOptions: {
maxAge: 7 * 24 * 60 * 60 * 1000, // Cookie lifetime in milliseconds
httpOnly: true, // Prevent client-side access
secure: true, // HTTPS only in production
sameSite: 'lax', // CSRF protection
}Web Auth Configuration
export const webAuth = wa({
tokenName: 'auth_token', // Token name in storage
authEndpoint: '/api/auth/authenticate/', // Authentication API endpoint
})Database Schema
Astro DB Schema
// db/config.ts
import { defineDb, defineTable, column } from 'astro:db'
const User = defineTable({
columns: {
id: column.text({ primaryKey: true }),
email: column.text({ unique: true }),
password: column.text(),
first_name: column.text({ optional: true }),
name: column.text({ optional: true }),
created_at: column.date({ default: new Date() }),
},
})
// Only needed for 'db' strategy
const Session = defineTable({
columns: {
id: column.text({ primaryKey: true }),
user_id: column.text({ references: () => User.columns.id }),
expires_at: column.date(),
created_at: column.date({ default: new Date() }),
},
})
const PasswordResetToken = defineTable({
columns: {
id: column.text({ primaryKey: true }),
user_id: column.text({ references: () => User.columns.id }),
token: column.text({ unique: true }),
expires_at: column.date(),
created_at: column.date({ default: new Date() }),
},
})
export default defineDb({
tables: { User, Session, PasswordResetToken },
})Drizzle Schema (SQLite)
// src/db/schema.js
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
import { sql } from 'drizzle-orm'
export const users = sqliteTable('users', {
id: text('id').primaryKey(),
email: text('email').unique().notNull(),
password: text('password').notNull(),
firstName: text('first_name'),
name: text('name'),
createdAt: integer('created_at', { mode: 'timestamp' }).default(
sql`CURRENT_TIMESTAMP`,
),
})
// Only needed for 'db' strategy
export const sessions = sqliteTable('sessions', {
id: text('id').primaryKey(),
userId: text('user_id')
.notNull()
.references(() => users.id),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).default(
sql`CURRENT_TIMESTAMP`,
),
})
export const passwordResetTokens = sqliteTable('password_reset_tokens', {
id: text('id').primaryKey(),
userId: text('user_id')
.notNull()
.references(() => users.id),
token: text('token').unique().notNull(),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).default(
sql`CURRENT_TIMESTAMP`,
),
})Drizzle Schema (PostgreSQL)
// src/db/schema.js
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { sql } from 'drizzle-orm'
export const users = pgTable('users', {
id: uuid('id').defaultRandom().primaryKey(),
email: text('email').unique().notNull(),
password: text('password').notNull(),
firstName: text('first_name'),
name: text('name'),
createdAt: timestamp('created_at').default(sql`CURRENT_TIMESTAMP`),
})
// Only needed for 'db' strategy
export const sessions = pgTable('sessions', {
id: uuid('id').defaultRandom().primaryKey(),
userId: uuid('user_id')
.notNull()
.references(() => users.id),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at').default(sql`CURRENT_TIMESTAMP`),
})
export const passwordResetTokens = pgTable('password_reset_tokens', {
id: uuid('id').defaultRandom().primaryKey(),
userId: uuid('user_id')
.notNull()
.references(() => users.id),
token: text('token').unique().notNull(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at').default(sql`CURRENT_TIMESTAMP`),
})Complete Drizzle Setup
1. Install Dependencies
npm install drizzle-orm better-sqlite3
npm install -D drizzle-kit @types/better-sqlite32. Database Configuration
// src/db/index.js
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import * as schema from './schema.js'
const sqlite = new Database('auth.db')
export const db = drizzle(sqlite, { schema })3. Drizzle Configuration
// drizzle.config.js
export default {
schema: './src/db/schema.js',
out: './drizzle',
driver: 'better-sqlite',
dbCredentials: {
url: './auth.db',
},
}4. Generate Migrations
# Generate migration
npx drizzle-kit generate:sqlite
# Run migration
npx drizzle-kit push:sqliteEnvironment Variables
# .env
DATABASE_URL=your_database_url
AUTH_SECRET=your_secret_key_for_cookie_signing
# For cookie strategy, AUTH_SECRET is required
# Generate a strong secret: openssl rand -base64 32Server Methods
auth.login(context, credentials)
await auth.login(Astro, { email: '[email protected]', password: 'password' })auth.authenticate(context)
const user = await auth.authenticate(Astro)auth.logout(context)
await auth.logout(Astro)auth.createUser(userData)
const user = await auth.createUser({
email: '[email protected]',
password: 'password',
first_name: 'John',
name: 'John Doe',
})Web/Client Methods
webAuth.isAuthenticated()
const isAuth = await webAuth.isAuthenticated()webAuth.getUser()
const user = await webAuth.getUser()webAuth.logout()
await webAuth.logout()Security Notes
CSRF Protection
CSRF protection is enabled by default in Astro v5.0:
// astro.config.mjs
export default defineConfig({
output: 'server',
security: { checkOrigin: true }, // Default in Astro 5.0
})Advanced Verification
Add custom verification logic:
export const auth = splAuth({
// ... other config
auth: {
strategy: 'db',
userAttributes: ['email', 'first_name', 'name'],
async additionalVerification(context, { user }) {
// Check subscriptions, permissions, etc.
const subscriptions = await getUserSubscriptions(user.id)
const permissions = await getUserPermissions(user.id)
return {
subscriptions,
permissions,
product_access: subscriptions.length > 0,
}
},
},
})Troubleshooting
Database Deployment with Astro DB
When using astro:db, you cannot use astro db execute <file-path> --remote directly. Instead:
- Run
npx astro dev --remote - Create an Astro page with your database operations
- Navigate to that page once to execute the operations
