dev-auth — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited dev-auth (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
| Solution | When to choose | Avoid when |
|---|---|---|
| better-auth | Total control, TS-first, extensible (plugins), native 2FA/passkeys | Project < 1 week MVP |
| Own sessions (the Lucia guide) | Minimalism, you own every line — Lucia the library was deprecated in March 2025, its author now publishes the session code as a guide | No time for plumbing |
| NextAuth/Auth.js | Next.js ecosystem, easy OAuth, lots of adapters | Need fine control over sessions |
| Clerk | Fast MVP, pre-built UI, paid SaaS | Limited budget, sovereign data control |
| Supabase Auth | Already on Supabase, RLS for authorization | Non-Postgres stack, complex custom auth |
| Auth0 / Okta | Enterprise, SAML/SCIM compliance | Indie apps, high cost |
IMPORTANT: Never roll your own auth (homemade JWT, custom password hashing). Use a maintained lib.
Framework-agnostic (Next, Remix, SvelteKit, Nuxt, vanilla). TypeScript-first.
npm install better-auth// lib/auth.ts
import { betterAuth } from "better-auth";
import { Pool } from "pg";
export const auth = betterAuth({
database: new Pool({ connectionString: process.env.DATABASE_URL }),
emailAndPassword: { enabled: true },
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
});// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient();
// Usage
const { data: session } = authClient.useSession();
await authClient.signIn.email({ email, password });
await authClient.signUp.email({ email, password, name });
await authClient.signOut();import { twoFactor, magicLink, passkey } from "better-auth/plugins";
betterAuth({
plugins: [
twoFactor(), // TOTP
magicLink({ sendMagicLink: ... }), // Email magic link
passkey(), // WebAuthn/passkeys
],
});The lucia package was deprecated in March 2025. Its author now maintains lucia-auth.com as a guide to implementing sessions yourself, with a single-file reference implementation (code/auth_session.ts in the lucia-auth/lucia repo) to copy and adapt. There is no installer: you copy the code into your codebase and own it. Its sessions live in a cookie, so CSRF protection is mandatory (the file's own header says so; SvelteKit and Astro enable it by default, other stacks must add it).
An existing project on the lucia package keeps working but gets no fixes: plan the move to that copied code or to better-auth.
npm install next-auth@beta// auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [GitHub],
});// app/api/auth/[...nextauth]/route.ts
export { GET, POST } from "@/auth";// middleware.ts
export { auth as middleware } from "@/auth";| Approach | Pro | Con |
|---|---|---|
| Session cookie (id → DB) | Immediate revocation, small cookie size | DB query on every check |
| Stateless JWT | No DB check, horizontal scale | Complex revocation, large cookie size |
| Session cookie + JWT refresh | Best of both | Complexity |
Recommended default: opaque session cookie stored in DB. Simpler, safer.
{
httpOnly: true, // JS cannot read the cookie (anti-XSS)
secure: true, // HTTPS only in prod
sameSite: "lax", // CSRF protection (strict if no OAuth)
path: "/",
maxAge: 60 * 60 * 24 * 7, // 7 days
}Never MD5, SHA-1, bcrypt < cost 12.
2026 recommendations:
argon2 package on npm.import argon2 from "argon2";
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // 19 MB
timeCost: 2,
parallelism: 1,
});
const valid = await argon2.verify(hash, password);IMPORTANT: libs like better-auth / NextAuth already hash correctly. Only reimplement if you're doing custom auth (and you shouldn't).
Always HTTPS in prod. Add http://localhost:3000/... for dev.
https://app.example.com/api/auth/callback/githubRequest only what you need:
read:user user:email (not repo if you don't read repos)openid email profileProtects against OAuth CSRF. Modern libs do this automatically.
Auth only verifies who the user is. For what they can do, you need roles/permissions.
| Pattern | Usage |
|---|---|
| RBAC (Role-Based) | Fixed roles: admin, user, viewer |
| ABAC (Attribute-Based) | Dynamic rules: "user can edit if owner" |
| RLS (Row-Level Security) | Postgres/Supabase: SQL policies per user |
| CASL / access-js | JS lib to express permissions |
// middleware.ts
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
export async function middleware(request: NextRequest) {
const session = await auth.api.getSession({ headers: request.headers });
if (!session && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
if (session?.user.role !== "admin" && request.nextUrl.pathname.startsWith("/admin")) {
return NextResponse.redirect(new URL("/403", request.url));
}
}TOTP (Google Authenticator) is the default.
// better-auth example
await authClient.twoFactor.enable({ password });
// Returns a QR code to scan
await authClient.twoFactor.verify({ code: "123456" });Passkeys (WebAuthn) is the future. Native passwordless.
| Pitfall | Prevention |
|---|---|
| Timing attack on password comparison | Use argon2.verify / constant-time compare |
| User enumeration via login | Same error message for "unknown email" and "incorrect password" |
| Session fixation | Regenerate session ID after login |
| CSRF | SameSite cookie + OAuth state + Origin check on mutations |
| XSS on token | httpOnly cookie (JS cannot read) |
| Brute force | Rate limit per IP + per account, captcha after N failures |
| Password reset leaks info | Same response "email sent" even if email doesn't exist |
| OAuth open redirect | Validate the redirect_uri against a whitelist |
httpOnly, secure, sameSite correct/login, /register, /reset-password| From → To | Strategy |
|---|---|
| NextAuth → better-auth | Dual-write sessions during the transition, batch user migration |
| Supabase Auth → better-auth | Export users + password hashes if compatible, otherwise force reset |
| Custom JWT → own sessions (Lucia guide) | Invalidate all JWTs, force re-login |
lucia package → own sessions (Lucia guide) | New session schema (secret_hash, token_last_verified_at): migrate the table, every user logs in again |
lucia package → better-auth | New session schema: migrate users, then force re-login |
IMPORTANT: Never migrate without a prior DB backup and rollback plan.
docs/STACK-RECIPES.md.claude/rules/security.md: OWASP Top 10qa-security: full security auditdev-supabase: if Supabase stackIMPORTANT: NEVER roll your own auth (homemade JWT, custom password hashing).
IMPORTANT: Session cookie MUST BE httpOnly + secure + sameSite.
IMPORTANT: Password hashing = argon2id (default) or bcrypt cost 12+ (legacy).
YOU MUST rate-limit /login, /register, /reset-password (5-10 attempts/15min).
YOU MUST return the same error messages for "unknown user" and "incorrect password" (anti-enumeration).
NEVER expose reset/verification tokens in logs or shared URLs.
NEVER store passwords in clear text, even temporarily.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.