Better Auth + Drizzle 기반 안전한 로그인: 이메일/비밀번호, GitHub/Google, 2FA, 속도 제한, CSRF
코드파일 6개컨텍스트약 812토큰MCP better-auth검사 통과
설치
$
genpm add @yohangel/auth포함 내용
- src/lib/auth/에 소스 코드, 파일 6개. (15.5kB)
- src/lib/auth/AGENTS.md에 AI 규칙, 그리고 IDE 규칙 파일.
- .env.example에 추가되는 환경 변수: BETTER_AUTH_SECRET, BETTER_AUTH_URL, DATABASE_URL.
README
이 패키지에는 README가 없습니다.
약 812토큰→ src/lib/auth/AGENTS.md→ .cursor/rules/genpm-yohangel-auth.mdc
이것이 AI가 src/lib/auth에서 작업할 때 읽는 내용 그대로입니다. 그 외에는 컨텍스트에 아무것도 추가되지 않습니다.
@yohangel/auth — rules for AI agents
Purpose
Server-side authentication core built on Better Auth + Drizzle (Postgres): email/password, GitHub/Google OAuth, TOTP 2FA, database-backed rate limiting and CSRF protection. Framework-agnostic: pair it with @yohangel/auth-next, auth-express or auth-nest (server) and @yohangel/auth-react (UI). It does not send emails by itself and has no UI.
Module map
index.ts— public API. Import only from here.auth.ts—createAuth(opts)(secure defaults),getAuth(),getSession(headers),socialProviders().schema.ts— Drizzle tables:users,sessions,accounts,verifications,two_factors,rate_limits.db.ts—setAuthDb(db)/getAuthDb(); defaults to postgres-js fromDATABASE_URL.
Integration (do this after installing)
- Set env vars (add them to
.env.example, never commit real values):BETTER_AUTH_SECRET— 32+ random chars (openssl rand -base64 32). Startup throws in production if shorter.BETTER_AUTH_URL— public base URL of the server, e.g.https://app.example.com.DATABASE_URL— Postgres. If the app already has a Drizzle client, pass it:createAuth({ db }).- Optional:
GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET,GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET(a provider is enabled only when both are set; callback URL is<BETTER_AUTH_URL>/api/auth/callback/<provider>),APP_NAME(2FA issuer),AUTH_TRUSTED_ORIGINS(comma-separated extra origins, e.g. a separate SPA),AUTH_IP_HEADER(e.g.cf-connecting-ipbehind Cloudflare, so rate limits see the real client IP).
- Add the tables to the app's migrations: include
src/lib/auth/schema.tsindrizzle.config.tsschema, thennpx drizzle-kit generate && npx drizzle-kit migrate. - Wire emails:
createAuth({ sendEmail: async ({ kind, to, url, subject, text }) => … })using the app's mailer. WithsendEmailset, email verification is required to sign in and password reset is enabled. Without it, links are printed to the console in development only. - Create the instance through the framework adapter package (it calls
createAuthwith the right plugins). UsegetSession(headers)anywhere else on the server.
Conventions
- One instance per app. Extra Better Auth plugins go in
createAuth({ plugins: [...] })— e.g.organization(),admin(),passkey()— then regenerate/extendschema.tswith the tables those plugins document. - Always authorize on the server with
getSession(); client-side checks are only for UX. - Prefer
overridesonly for options not covered here, and keep the security defaults below.
Don't
- Don't lower
minPasswordLength(12), disablerateLimitin production, or setadvanced.disableCSRFCheck/disableOriginChecktotrue. - Don't log, return or store passwords, session tokens, OAuth tokens or
BETTER_AUTH_SECRET. - Don't add origins to
AUTH_TRUSTED_ORIGINSwith wildcards you do not control. - Don't read
accounts.passwordor compare passwords yourself; use Better Auth's API. - Don't build redirects from user input without checking they are internal paths.
Docs: the Better Auth MCP server (better-auth) is configured by GenPM; ask it about plugins and options.
# @yohangel/auth — rules for AI agents
## Purpose
Server-side authentication core built on Better Auth + Drizzle (Postgres): email/password, GitHub/Google OAuth, TOTP 2FA, database-backed rate limiting and CSRF protection. Framework-agnostic: pair it with `@yohangel/auth-next`, `auth-express` or `auth-nest` (server) and `@yohangel/auth-react` (UI). It does not send emails by itself and has no UI.
## Module map
- `index.ts` — public API. Import only from here.
- `auth.ts` — `createAuth(opts)` (secure defaults), `getAuth()`, `getSession(headers)`, `socialProviders()`.
- `schema.ts` — Drizzle tables: `users`, `sessions`, `accounts`, `verifications`, `two_factors`, `rate_limits`.
- `db.ts` — `setAuthDb(db)` / `getAuthDb()`; defaults to postgres-js from `DATABASE_URL`.
## Integration (do this after installing)
1. Set env vars (add them to `.env.example`, never commit real values):
- `BETTER_AUTH_SECRET` — 32+ random chars (`openssl rand -base64 32`). Startup throws in production if shorter.
- `BETTER_AUTH_URL` — public base URL of the server, e.g. `https://app.example.com`.
- `DATABASE_URL` — Postgres. If the app already has a Drizzle client, pass it: `createAuth({ db })`.
- Optional: `GITHUB_CLIENT_ID`/`GITHUB_CLIENT_SECRET`, `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET` (a provider is enabled only when both are set; callback URL is `<BETTER_AUTH_URL>/api/auth/callback/<provider>`), `APP_NAME` (2FA issuer), `AUTH_TRUSTED_ORIGINS` (comma-separated extra origins, e.g. a separate SPA), `AUTH_IP_HEADER` (e.g. `cf-connecting-ip` behind Cloudflare, so rate limits see the real client IP).
2. Add the tables to the app's migrations: include `src/lib/auth/schema.ts` in `drizzle.config.ts` `schema`, then `npx drizzle-kit generate && npx drizzle-kit migrate`.
3. Wire emails: `createAuth({ sendEmail: async ({ kind, to, url, subject, text }) => … })` using the app's mailer. With `sendEmail` set, email verification is required to sign in and password reset is enabled. Without it, links are printed to the console in development only.
4. Create the instance through the framework adapter package (it calls `createAuth` with the right plugins). Use `getSession(headers)` anywhere else on the server.
## Conventions
- One instance per app. Extra Better Auth plugins go in `createAuth({ plugins: [...] })` — e.g. `organization()`, `admin()`, `passkey()` — then regenerate/extend `schema.ts` with the tables those plugins document.
- Always authorize on the server with `getSession()`; client-side checks are only for UX.
- Prefer `overrides` only for options not covered here, and keep the security defaults below.
## Don't
- Don't lower `minPasswordLength` (12), disable `rateLimit` in production, or set `advanced.disableCSRFCheck` / `disableOriginCheck` to `true`.
- Don't log, return or store passwords, session tokens, OAuth tokens or `BETTER_AUTH_SECRET`.
- Don't add origins to `AUTH_TRUSTED_ORIGINS` with wildcards you do not control.
- Don't read `accounts.password` or compare passwords yourself; use Better Auth's API.
- Don't build redirects from user input without checking they are internal paths.
Docs: the Better Auth MCP server (`better-auth`) is configured by GenPM; ask it about plugins and options.
.genpmignore 적용 후 주입될 정확한 트리입니다. 고정 대상:
// Instancia de Better Auth con valores seguros por defecto. Los adaptadores de framework (auth-next, auth-express,
// auth-nest) llaman a `createAuth()` con sus plugins; el resto del código usa `getAuth()` / `getSession()`.
import { drizzleAdapter } from '@better-auth/drizzle-adapter';
import { type BetterAuthOptions, type BetterAuthPlugin, betterAuth } from 'better-auth';
import { twoFactor } from 'better-auth/plugins';
import { type AuthDb, getAuthDb, setAuthDb } from './db.js';
import { authSchema } from './schema.js';
/** Correo que Better Auth necesita enviar. `subject` y `text` son un texto por defecto en inglés; usa `kind` y `url`
* para tus propias plantillas (p. ej. Resend o tu mailer). */
export type AuthEmail = {
kind: 'verify-email' | 'reset-password';
to: string;
url: string;
subject: string;
text: string;
};
export type CreateAuthOptions = {
/** Tu cliente Drizzle (Postgres). Por defecto, uno creado desde DATABASE_URL. */
db?: AuthDb;
/** Plugins extra de Better Auth (organization, passkey, magicLink…). `nextCookies()` siempre el último. */
plugins?: BetterAuthPlugin[];
/** Envío de correos de verificación y de restablecer contraseña. Sin él: en desarrollo se imprimen los enlaces
* en consola; en producción no se exige verificar el email y restablecer contraseña queda desactivado. */
sendEmail?: (email: AuthEmail) => Promise<void>;
/** Sobrescribe cualquier opción de Better Auth (se aplica al final; úsalo con cuidado). */
overrides?: Partial<BetterAuthOptions>;
};
const isProd = () => process.env.NODE_ENV === 'production';
const list = (v: string | undefined) =>
(v ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
function checkSecret(): void {
const secret = process.env.BETTER_AUTH_SECRET ?? '';
if (isProd() && secret.length < 32)
throw new Error('BETTER_AUTH_SECRET must be at least 32 characters (openssl rand -base64 32)');
}
/** Proveedores OAuth activos: solo los que tienen client id y secret en el entorno. */
export function socialProviders(): NonNullable<BetterAuthOptions['socialProviders']> {
const out: NonNullable<BetterAuthOptions['socialProviders']> = {};
const e = process.env;
if (e.GITHUB_CLIENT_ID && e.GITHUB_CLIENT_SECRET)
out.github = { clientId: e.GITHUB_CLIENT_ID, clientSecret: e.GITHUB_CLIENT_SECRET };
if (e.GOOGLE_CLIENT_ID && e.GOOGLE_CLIENT_SECRET)
out.google = { clientId: e.GOOGLE_CLIENT_ID, clientSecret: e.GOOGLE_CLIENT_SECRET };
return out;
}
export function createAuth(opts: CreateAuthOptions = {}) {
checkSecret();
if (opts.db) setAuthDb(opts.db);
const appName = process.env.APP_NAME ?? 'App';
const send =
opts.sendEmail ??
(isProd()
? null
: async (m: AuthEmail) => {
console.info(`[auth] ${m.kind} for ${m.to}: ${m.url}`);
});
const ipHeader = process.env.AUTH_IP_HEADER;
const options = {
appName,
database: drizzleAdapter(getAuthDb(), { provider: 'pg', schema: authSchema }),
trustedOrigins: list(process.env.AUTH_TRUSTED_ORIGINS),
emailAndPassword: {
enabled: true,
// NIST 800-63B: mínimo largo antes que reglas de composición. Hash scrypt (por defecto en Better Auth).
minPasswordLength: 12,
maxPasswordLength: 128,
requireEmailVerification: !!opts.sendEmail,
revokeSessionsOnPasswordReset: true,
...(send
? {
sendResetPassword: async ({ user, url }: { user: { email: string }; url: string }) =>
send({
kind: 'reset-password',
to: user.email,
url,
subject: `Reset your ${appName} password`,
text: `Open this link to choose a new password (valid for 1 hour): ${url}\n\nIf you did not ask for it, ignore this email.`,
}),
}
: {}),
},
...(send
? {
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
sendVerificationEmail: async ({ user, url }: { user: { email: string }; url: string }) =>
send({
kind: 'verify-email',
to: user.email,
url,
subject: `Verify your ${appName} email`,
text: `Confirm your email address: ${url}`,
}),
},
}
: {}),
socialProviders: socialProviders(),
session: {
expiresIn: 60 * 60 * 24 * 30,
updateAge: 60 * 60 * 24,
// Caché firmada en cookie: menos lecturas de BD; una sesión revocada deja de valer en ≤ 5 min.
cookieCache: { enabled: true, maxAge: 5 * 60 },
},
rateLimit: {
enabled: isProd() || process.env.AUTH_RATE_LIMIT === '1',
storage: 'database',
window: 60,
max: 100,
customRules: {
'/sign-in/email': { window: 60, max: 5 },
'/sign-up/email': { window: 60, max: 3 },
'/request-password-reset': { window: 300, max: 3 },
'/two-factor/*': { window: 60, max: 5 },
},
},
advanced: {
// Explícitos: Better Auth los desactiva solo si NODE_ENV=test; aquí la protección CSRF nunca depende del entorno.
disableCSRFCheck: false,
disableOriginCheck: false,
...(ipHeader ? { ipAddress: { ipAddressHeaders: [ipHeader] } } : {}),
},
plugins: [twoFactor({ issuer: appName }), ...(opts.plugins ?? [])],
...opts.overrides,
} satisfies BetterAuthOptions;
const instance = betterAuth(options);
current = instance as unknown as Auth;
return instance;
}
export type Auth = ReturnType<typeof createAuth>;
let current: Auth | null = null;
/** La instancia de la app (la última creada con `createAuth`, o una por defecto sin plugins extra). */
export function getAuth(): Auth {
current ??= createAuth();
return current;
}
export type AuthSession = NonNullable<Awaited<ReturnType<Auth['api']['getSession']>>>;
/** Sesión y usuario de una petición a partir de sus cabeceras (null si no hay sesión válida). */
export async function getSession(headers: Headers): Promise<AuthSession | null> {
return getAuth().api.getSession({ headers });
}
- 서버
- better-auth
- 명령
- https://mcp.better-auth.com/mcp
| 버전 | 커밋 | 게시일 | 검사 |
|---|---|---|---|
| 1.0.0 | 7145b9d | 3시간 전 | ✔ 검사 통과 |
- genpm
- 없음
- 제안됨
- GenPM은 npm 명령을 제안하고, 동의한 경우에만 실행합니다.
- 검사
- 검사 통과 · 문제 0건
- 커밋
- auth@1.0.0 → 7145b9d58055b5145085782c958b7577f1d65276 · 가져온 뒤 검증됨
- 스크립트
- 없음. GenPM은 패키지 코드를 절대 실행하지 않습니다.
- 라이선스
- MIT
- 신고
- 문제가 있나요?