Skip to content

Auth Setup

Experimental: @lastshotlabs/slingshot-auth is currently pre-1.0 (0.0.2) in this worktree. Read Auth and Maturity and Package Status before you commit to this package in a product.

Add slingshot-auth and you get registration, login, session management, password reset, OAuth, MFA, and passkeys. All from one plugin, zero boilerplate.

Source-backed example app: examples/with-auth/

Terminal window
bun add @lastshotlabs/slingshot @lastshotlabs/slingshot-auth hono zod
app.config.ts
import { defineApp } from '@lastshotlabs/slingshot';
import { createAuthPlugin } from '@lastshotlabs/slingshot-auth';
export default defineApp({
port: 3000,
security: {
signing: { secret: process.env.JWT_SECRET! },
},
plugins: [
createAuthPlugin({
auth: { roles: ['user', 'admin'], defaultRole: 'user' },
db: { auth: 'memory', sessions: 'memory', oauthState: 'memory' },
}),
],
});
MethodPathDescription
POST/auth/registerCreate an account
POST/auth/loginLog in, receive a JWT
POST/auth/logoutEnd the session
GET/auth/meCurrent user (requires JWT)
PATCH/auth/meUpdate profile fields
POST/auth/refreshRotate the session token
POST/auth/password-reset/requestSend a password reset email
POST/auth/password-reset/confirmComplete the reset

Enabling OAuth, MFA, or passkeys adds additional routes (see below).

Import userAuth and requireRole from slingshot-auth and apply them as Hono middleware before your handler.

src/routes/account.ts
import { createRoute, createRouter, getActor } from '@lastshotlabs/slingshot';
import { requireRole, userAuth } from '@lastshotlabs/slingshot-auth';
declare function getUserCount(): Promise<number>;
export const router = createRouter();
// Require a valid JWT — returns 401 automatically if missing or invalid
router.use('/account/*', userAuth);
router.openapi(
createRoute({
method: 'get',
path: '/account/profile',
responses: {
200: {
description: 'Current user',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
userId: { type: 'string' },
roles: {
type: 'array',
items: { type: 'string' },
},
},
required: ['userId', 'roles'],
},
},
},
},
},
}),
// @ts-expect-error -- simplified doc example, handler type is narrower than openapi expects
c => c.json({ userId: getActor(c).id!, roles: getActor(c).roles ?? [] }),
);
// Require a specific role — returns 403 if the user doesn't have it
router.use('/account/admin/*', requireRole('admin'));
router.openapi(
createRoute({
method: 'get',
path: '/account/admin/stats',
responses: {
200: {
description: 'Admin stats',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
userCount: { type: 'number' },
},
required: ['userCount'],
},
},
},
},
},
}),
async c => {
const count = await getUserCount();
return c.json({ userCount: count }) as never;
},
);

userAuth verifies the JWT from the Authorization: Bearer header or the x-user-token header and sets the actor on the context. requireRole checks the actor’s roles value — always mount it after userAuth.

Register, log in, then pass the token as a Bearer header:

Terminal window
# Register
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{ "email": "you@example.com", "password": "hunter2" }'
# Log in
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{ "email": "you@example.com", "password": "hunter2" }'
# → { "token": "eyJ..." }
# Call a protected route
curl http://localhost:3000/account/profile \
-H "Authorization: Bearer eyJ..."

Each adapter is independent. Mix and match:

app.config.ts (excerpt)
export default defineApp({
db: { sqlite: `${import.meta.dir}/data/app.db` },
plugins: [
createAuthPlugin({
auth: { roles: ['user', 'admin'], defaultRole: 'user' },
db: { auth: 'sqlite', sessions: 'sqlite', oauthState: 'sqlite' },
}),
],
});

SQLite is built into Bun. On Node.js, install better-sqlite3 first.

Terminal window
bun add @lastshotlabs/slingshot-oauth arctic
createAuthPlugin({
auth: {
roles: ['user'],
defaultRole: 'user',
oauth: {
providers: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: 'http://localhost:3000/auth/oauth/google/callback',
},
},
postRedirect: '/dashboard',
},
},
db: { auth: 'memory', sessions: 'memory', oauthState: 'memory' },
});

OAuth routes mount automatically: GET /auth/oauth/:providerGET /auth/oauth/:provider/callback. Redirect the user to /auth/oauth/google to start the flow. After the callback, they have a session.

Terminal window
bun add otpauth
auth: { roles: ['user'], defaultRole: 'user', mfa: { totp: true } }

Adds POST /auth/mfa/setup, POST /auth/mfa/verify, and DELETE /auth/mfa.

Terminal window
bun add @simplewebauthn/server
auth: {
roles: ['user'],
defaultRole: 'user',
passkeys: {
rpName: 'My App',
rpId: 'localhost',
origin: 'http://localhost:3000',
},
}

Adds begin/complete routes for both registration and authentication.

When emailVerification.required is enabled, passkey authentication follows the same gate as password login: unverified email accounts cannot sign in with WebAuthn until the address is verified.

Require users to verify their email before they can log in:

auth: {
roles: ['user'],
defaultRole: 'user',
emailVerification: {
required: true,
tokenExpirySeconds: 86400, // 24 hours
},
}
auth: {
passwordPolicy: {
minLength: 12,
requireUppercase: true,
requireNumbers: true,
},
}

Declare all roles upfront. Auth stores them in the JWT. Access them via getActor(c).roles:

auth: { roles: ['user', 'moderator', 'admin'], defaultRole: 'user' }

Assign roles through the admin plugin or directly through the auth adapter in your own routes.

  • slingshot-auth — full config reference with all options
  • Forum App — add slingshot-community on top of auth
  • SaaS Foundations — add organizations and per-resource permissions
  • OpenAPI — how userAuth and requireRole appear in the spec