Sonamuλ better-authλ₯Ό κΈ°λ°μΌλ‘ νλ μΈμ¦ μμ€ν
μ μ 곡ν©λλ€. μ΄λ©μΌ/λΉλ°λ²νΈ μΈμ¦, μμ
λ‘κ·ΈμΈ λ± λ€μν μΈμ¦ λ°©μμ μ§μνλ©°, /api/auth/* κ²½λ‘λ‘ μΈμ¦ APIκ° μλ λ±λ‘λ©λλ€.
기본 ꡬ쑰
import { defineConfig } from "sonamu";
export default defineConfig({
server: {
// κΈ°λ³Έ μ€μ μΌλ‘ μΈμ¦ νμ±ν
auth: {
emailAndPassword: {
enabled: true,
},
},
// λλ μμΈ μ€μ
auth: {
basePath: "/api/auth",
emailAndPassword: {
enabled: true,
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
},
},
// ...
});
auth μ€μ
νμ
: BetterAuthOptions (better-auth λΌμ΄λΈλ¬λ¦¬μ μ€μ νμ
)
export default defineConfig({
server: {
auth: {
// better-auth μ€μ μ΅μ
},
},
});
μ΄λ©μΌ/λΉλ°λ²νΈ μΈμ¦
export default defineConfig({
server: {
auth: {
emailAndPassword: {
enabled: true,
// μ ν: λΉλ°λ²νΈ μ΅μ κΈΈμ΄
minPasswordLength: 8,
},
},
},
});
μμ
λ‘κ·ΈμΈ (Google)
export default defineConfig({
server: {
auth: {
emailAndPassword: {
enabled: true,
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
},
},
});
μμ
λ‘κ·ΈμΈ (GitHub)
export default defineConfig({
server: {
auth: {
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
},
},
});
basePath 컀μ€ν°λ§μ΄μ§
κΈ°λ³Έ κ²½λ‘λ /api/authμ
λλ€. λ³κ²½μ΄ νμν κ²½μ°:
export default defineConfig({
server: {
auth: {
basePath: "/auth", // /auth/* λ‘ λ³κ²½
emailAndPassword: {
enabled: true,
},
},
},
});
μν°ν° μμ±
better-authλ₯Ό μ¬μ©νλ €λ©΄ λ¨Όμ νμν μν°ν°λ€μ μμ±ν΄μΌ ν©λλ€.
CLIλ‘ μν°ν° μμ±
μ΄ λͺ
λ Ήμ λ€μ μν°ν°λ€μ μμ±ν©λλ€:
| μν°ν° | ν
μ΄λΈ | μ€λͺ
|
|---|
| User | users | μ¬μ©μ μ 보 |
| Session | sessions | μΈμ
μ 보 |
| Account | accounts | OAuth κ³μ μ°λ μ 보 |
| Verification | verifications | μ΄λ©μΌ μΈμ¦ λ± |
μ΄λ―Έ User μν°ν°κ° μλ κ²½μ°, λͺ
λ Ή μ€ν μ λλ½λ νλλ§ μΆκ°λ©λλ€.
νλ¬κ·ΈμΈκ³Ό ν¨κ» μν°ν° μμ±
better-auth νλ¬κ·ΈμΈμ μ¬μ©νλ €λ©΄ --plugins μ΅μ
μΌλ‘ νμν νλ¬κ·ΈμΈλ€μ μ§μ ν©λλ€:
# λ¨μΌ νλ¬κ·ΈμΈ
pnpm sonamu better-auth --plugins=2fa
# μ¬λ¬ νλ¬κ·ΈμΈ (μΌνλ‘ κ΅¬λΆ)
pnpm sonamu better-auth --plugins=2fa,admin,username
μ§μνλ νλ¬κ·ΈμΈ:
| νλ¬κ·ΈμΈ ID | μ€λͺ
| μΆκ°λλ νλ/ν
μ΄λΈ |
|---|
2fa | 2λ¨κ³ μΈμ¦ (TOTP) | TwoFactor ν
μ΄λΈ, User.two_factor_enabled |
admin | κ΄λ¦¬μ κΈ°λ₯ | User.role, User.banned, User.ban_reason, User.ban_expires, Session.impersonated_by |
username | μ¬μ©μλͺ
λ‘κ·ΈμΈ | User.username (unique), User.display_username |
phone-number | μ νλ²νΈ μΈμ¦ | User.phone_number (unique), User.phone_number_verified |
passkey | WebAuthn ν¨μ€ν€ μΈμ¦ | Passkey ν
μ΄λΈ |
sso | SSO λ‘κ·ΈμΈ (OIDC/SAML) | SsoProvider ν
μ΄λΈ |
api-key | API ν€ μΈμ¦ | ApiKey ν
μ΄λΈ |
jwt | JWT ν ν° λ°κΈ | Jwks ν
μ΄λΈ |
organization | μ‘°μ§/ν κ΄λ¦¬ | Organization, Member, Invitation, Team, TeamMember ν
μ΄λΈ, Session.active_organization_id, Session.active_team_id |
anonymous | μ΅λͺ
μ¬μ©μ | User.is_anonymous |
νλ¬κ·ΈμΈ μ¬μ© μ sonamu.config.tsμμλ ν΄λΉ νλ¬κ·ΈμΈμ νμ±νν΄μΌ ν©λλ€. μμΈν λ΄μ©μ μλ
νλ¬κ·ΈμΈ μ€μ μΉμ
μ μ°Έμ‘°νμΈμ.
νλ λ§€ν
Sonamuλ snake_case 컬λΌλͺ
μ μ¬μ©νλ―λ‘, better-authμ camelCase νλλͺ
μ΄ μλμΌλ‘ λ§€νλ©λλ€:
// better-auth β Sonamu
emailVerified β email_verified
createdAt β created_at
updatedAt β updated_at
ipAddress β ip_address
userAgent β user_agent
userId β user_id
// ...
μΈμ¦ API
better-authκ° λ±λ‘λλ©΄ λ€μ APIλ€μ΄ μλμΌλ‘ μ¬μ© κ°λ₯ν©λλ€:
νμκ°μ
POST /api/auth/sign-up/email
Content-Type: application/json
{
"name": "νκΈΈλ",
"email": "user@example.com",
"password": "password123"
}
λ‘κ·ΈμΈ
POST /api/auth/sign-in/email
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123"
}
λ‘κ·Έμμ
νμ¬ μΈμ
GET /api/auth/get-session
μμ
λ‘κ·ΈμΈ (Google)
GET /api/auth/sign-in/social?provider=google
Contextμμ μ¬μ©μ μ 보 μ κ·Ό
μΈμ¦λ μμ²μμλ Contextλ₯Ό ν΅ν΄ μ¬μ©μ μ 보μ μ κ·Όν μ μμ΅λλ€.
import { api, Sonamu } from "sonamu";
export class MyModel {
@api()
static async myApi() {
const ctx = Sonamu.getContext();
// νμ¬ λ‘κ·ΈμΈν μ¬μ©μ
const user = ctx.user; // User | null
// νμ¬ μΈμ
μ 보
const session = ctx.session; // Session | null
if (!user) {
throw new UnauthorizedError("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
return { userId: user.id, userName: user.name };
}
}
User νμ
type User = {
id: string;
name: string;
email: string;
emailVerified: boolean;
image: string | null;
createdAt: Date;
updatedAt: Date;
};
Session νμ
type Session = {
id: string;
expiresAt: Date;
token: string;
createdAt: Date;
updatedAt: Date;
ipAddress: string | null;
userAgent: string | null;
userId: string;
};
Guardλ₯Ό μ΄μ©ν μ κ·Ό μ μ΄
import { api } from "sonamu";
export class AdminModel {
@api({ guards: ["admin"] })
static async adminOnly() {
// admin guardκ° ν΅κ³Όν κ²½μ°μλ§ μ€ν
return { message: "κ΄λ¦¬μ μ μ© API" };
}
}
guardHandler μ€μ :
export default defineConfig({
server: {
auth: {
emailAndPassword: { enabled: true },
},
apiConfig: {
guardHandler: async (guard, request) => {
// Contextμμ user κ°μ Έμ€κΈ°
const { user } = Sonamu.getContext();
if (guard === "auth" && !user) {
throw new UnauthorizedError("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
if (guard === "admin") {
if (!user) {
throw new UnauthorizedError("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
// User μν°ν°μ role νλκ° μλ€κ³ κ°μ
const fullUser = await UserModel.findById("A", user.id);
if (fullUser.role !== "admin") {
throw new UnauthorizedError("κ΄λ¦¬μ κΆνμ΄ νμν©λλ€");
}
}
},
},
},
});
ν΄λΌμ΄μΈνΈ μΈ‘ μ°λ
Reactμμ μ¬μ©
// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: "http://localhost:4000",
});
// components/LoginForm.tsx
import { authClient } from "../lib/auth-client";
export function LoginForm() {
const handleLogin = async (email: string, password: string) => {
const result = await authClient.signIn.email({
email,
password,
});
if (result.error) {
alert(result.error.message);
return;
}
// λ‘κ·ΈμΈ μ±κ³΅
window.location.href = "/dashboard";
};
// ...
}
// components/GoogleLoginButton.tsx
import { authClient } from "../lib/auth-client";
export function GoogleLoginButton() {
const handleGoogleLogin = () => {
authClient.signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
};
return <button onClick={handleGoogleLogin}>Googleλ‘ λ‘κ·ΈμΈ</button>;
}
νλ¬κ·ΈμΈ μ€μ
better-auth νλ¬κ·ΈμΈμ μ¬μ©νλ €λ©΄ sonamu.config.tsμ auth.plugins λ°°μ΄μ νλ¬κ·ΈμΈμ μΆκ°ν©λλ€.
νλ¬κ·ΈμΈμ μ€μ νκΈ° μ μ λ¨Όμ pnpm sonamu better-auth --plugins=... λͺ
λ ΉμΌλ‘ νμν μν°ν°μ
νλλ₯Ό μμ±ν΄μΌ ν©λλ€.
2λ¨κ³ μΈμ¦ (2FA)
TOTP κΈ°λ° 2λ¨κ³ μΈμ¦μ νμ±νν©λλ€:
import { defineConfig } from "sonamu";
import { twoFactor, TWO_FACTOR_SCHEMA } from "sonamu/auth";
export default defineConfig({
server: {
auth: {
emailAndPassword: { enabled: true },
plugins: [
twoFactor({
issuer: "My App", // OTP μ±μ νμλ μ΄λ¦
schema: TWO_FACTOR_SCHEMA,
}),
],
},
},
});
2FA κ΄λ ¨ API:
POST /api/auth/two-factor/enable - 2FA νμ±ν μμ
POST /api/auth/two-factor/verify - 2FA μ½λ κ²μ¦
POST /api/auth/two-factor/disable - 2FA λΉνμ±ν
κ΄λ¦¬μ νλ¬κ·ΈμΈ (Admin)
μ¬μ©μ μν , μ°¨λ¨ κΈ°λ₯, λ리 λ‘κ·ΈμΈ(impersonation)μ μ§μν©λλ€:
import { defineConfig } from "sonamu";
import { admin, ADMIN_SCHEMA } from "sonamu/auth";
export default defineConfig({
server: {
auth: {
emailAndPassword: { enabled: true },
plugins: [
admin({
schema: ADMIN_SCHEMA,
}),
],
},
},
});
Admin νλ¬κ·ΈμΈμ΄ User ν
μ΄λΈμ μΆκ°νλ νλ:
role - μ¬μ©μ μν (κΈ°λ³Έκ°: βuserβ)
banned - μ°¨λ¨ μ¬λΆ
ban_reason - μ°¨λ¨ μ¬μ
ban_expires - μ°¨λ¨ λ§λ£ μκ° (Unix timestamp)
μ¬μ©μλͺ
νλ¬κ·ΈμΈ (Username)
μ΄λ©μΌ λμ μ¬μ©μλͺ
μΌλ‘ λ‘κ·ΈμΈν μ μμ΅λλ€:
import { defineConfig } from "sonamu";
import { username, USERNAME_SCHEMA } from "sonamu/auth";
export default defineConfig({
server: {
auth: {
plugins: [
username({
schema: USERNAME_SCHEMA,
}),
],
},
},
});
Username νλ¬κ·ΈμΈμ΄ User ν
μ΄λΈμ μΆκ°νλ νλ:
username - μ κ·νλ μ¬μ©μλͺ
(μλ¬Έμ, unique μΈλ±μ€)
display_username - νμμ© μ¬μ©μλͺ
(μλ³Έ μΌμ΄μ€ μ μ§)
μ νλ²νΈ νλ¬κ·ΈμΈ (Phone Number)
μ νλ²νΈ μΈμ¦μ μ§μν©λλ€:
import { defineConfig } from "sonamu";
import { phoneNumber, PHONE_NUMBER_SCHEMA } from "sonamu/auth";
export default defineConfig({
server: {
auth: {
plugins: [
phoneNumber({
schema: PHONE_NUMBER_SCHEMA,
}),
],
},
},
});
Phone Number νλ¬κ·ΈμΈμ΄ User ν
μ΄λΈμ μΆκ°νλ νλ:
phone_number - μ νλ²νΈ (unique μΈλ±μ€)
phone_number_verified - μ νλ²νΈ μΈμ¦ μ¬λΆ
ν¨μ€ν€ νλ¬κ·ΈμΈ (Passkey)
WebAuthn/FIDO2 κΈ°λ° ν¨μ€ν€ μΈμ¦μ μ§μν©λλ€:
import { defineConfig } from "sonamu";
import { passkey, PASSKEY_SCHEMA } from "sonamu/auth";
export default defineConfig({
server: {
auth: {
emailAndPassword: { enabled: true },
plugins: [
passkey({
schema: PASSKEY_SCHEMA,
}),
],
},
},
});
Passkey νλ¬κ·ΈμΈμ΄ μμ±νλ ν
μ΄λΈ:
passkeys - μ¬μ©μμ ν¨μ€ν€ μ 보 (곡κ°ν€, μ격 μ¦λͺ
ID λ±)
Passkey κ΄λ ¨ API:
POST /api/auth/passkey/generate-register-options - ν¨μ€ν€ λ±λ‘ μ΅μ
μμ±
POST /api/auth/passkey/verify-registration - ν¨μ€ν€ λ±λ‘ κ²μ¦
POST /api/auth/passkey/generate-authentication-options - ν¨μ€ν€ μΈμ¦ μ΅μ
μμ±
POST /api/auth/passkey/verify-authentication - ν¨μ€ν€ μΈμ¦ κ²μ¦
Passkey νλ¬κ·ΈμΈ μ¬μ© μ @better-auth/passkey ν¨ν€μ§κ° νμν©λλ€.
SSO νλ¬κ·ΈμΈ
μΈλΆ IdP(OIDC, SAML)λ₯Ό ν΅ν SSO λ‘κ·ΈμΈμ μ§μν©λλ€:
import { defineConfig } from "sonamu";
import { sso, SSO_SCHEMA } from "sonamu/auth";
export default defineConfig({
server: {
auth: {
plugins: [
sso({
...SSO_SCHEMA,
}),
],
},
},
});
SSO νλ¬κ·ΈμΈμ΄ μμ±νλ ν
μ΄λΈ:
sso_providers - SSO μ 곡μ μ€μ (OIDC/SAML μ€μ ν¬ν¨)
SSO νλ¬κ·ΈμΈ μ¬μ© μ @better-auth/sso ν¨ν€μ§κ° νμν©λλ€.
API ν€ νλ¬κ·ΈμΈ (API Key)
API ν€ κΈ°λ° μΈμ¦μ μ§μν©λλ€:
import { defineConfig } from "sonamu";
import { apiKey, API_KEY_SCHEMA } from "sonamu/auth";
export default defineConfig({
server: {
auth: {
plugins: [
apiKey({
schema: API_KEY_SCHEMA,
}),
],
},
},
});
API Key νλ¬κ·ΈμΈμ΄ μμ±νλ ν
μ΄λΈ:
api_keys - API ν€ μ 보 (ν΄μλ ν€, Rate Limit μ€μ λ±)
API Key κ΄λ ¨ API:
POST /api/auth/api-key/create - API ν€ μμ±
POST /api/auth/api-key/revoke - API ν€ νκΈ°
GET /api/auth/api-key/list - API ν€ λͺ©λ‘ μ‘°ν
JWT νλ¬κ·ΈμΈ
JWT ν ν° λ°κΈ λ° JWKS ν€ κ΄λ¦¬λ₯Ό μ§μν©λλ€:
import { defineConfig } from "sonamu";
import { jwt, JWT_SCHEMA } from "sonamu/auth";
export default defineConfig({
server: {
auth: {
plugins: [
jwt({
schema: JWT_SCHEMA,
}),
],
},
},
});
JWT νλ¬κ·ΈμΈμ΄ μμ±νλ ν
μ΄λΈ:
jwks - JSON Web Key Set μ 보 (곡κ°ν€, λΉλ°ν€)
JWT κ΄λ ¨ API:
GET /api/auth/.well-known/jwks.json - JWKS μλν¬μΈνΈ
POST /api/auth/jwt/generate - JWT ν ν° μμ±
μ‘°μ§ νλ¬κ·ΈμΈ (Organization)
μ‘°μ§, λ©€λ², μ΄λ, ν κ΄λ¦¬λ₯Ό μ§μν©λλ€:
import { defineConfig } from "sonamu";
import { organization, ORGANIZATION_SCHEMA } from "sonamu/auth";
export default defineConfig({
server: {
auth: {
plugins: [
organization({
schema: ORGANIZATION_SCHEMA,
}),
],
},
},
});
Organization νλ¬κ·ΈμΈμ΄ μμ±νλ ν
μ΄λΈ:
organizations - μ‘°μ§ μ 보
members - μ‘°μ§ λ©€λ²
invitations - μ‘°μ§ μ΄λ
teams - ν
team_members - ν λ©€λ²
Organization νλ¬κ·ΈμΈμ΄ Session ν
μ΄λΈμ μΆκ°νλ νλ:
active_organization_id - νμ¬ νμ± μ‘°μ§ ID
active_team_id - νμ¬ νμ± ν ID
Organization κ΄λ ¨ API:
POST /api/auth/organization/create - μ‘°μ§ μμ±
POST /api/auth/organization/invite - λ©€λ² μ΄λ
POST /api/auth/organization/accept-invitation - μ΄λ μλ½
POST /api/auth/organization/set-active - νμ± μ‘°μ§ μ€μ
μ΅λͺ
μ¬μ©μ νλ¬κ·ΈμΈ (Anonymous)
μ΅λͺ
μ¬μ©μ μΈμ¦μ μ§μν©λλ€. νμκ°μ
μμ΄ μμ μ¬μ©μλ₯Ό μμ±ν μ μμ΅λλ€:
import { defineConfig } from "sonamu";
import { anonymous, ANONYMOUS_SCHEMA } from "sonamu/auth";
export default defineConfig({
server: {
auth: {
plugins: [
anonymous({
schema: ANONYMOUS_SCHEMA,
}),
],
},
},
});
Anonymous νλ¬κ·ΈμΈμ΄ User ν
μ΄λΈμ μΆκ°νλ νλ:
is_anonymous - μ΅λͺ
μ¬μ©μ μ¬λΆ
Anonymous κ΄λ ¨ API:
POST /api/auth/sign-in/anonymous - μ΅λͺ
λ‘κ·ΈμΈ
POST /api/auth/anonymous/link - μ΅λͺ
κ³μ μ μ μ κ³μ μΌλ‘ μ°κ²°
μ¬λ¬ νλ¬κ·ΈμΈ ν¨κ» μ¬μ©
import { defineConfig } from "sonamu";
import {
admin,
ADMIN_SCHEMA,
twoFactor,
TWO_FACTOR_SCHEMA,
username,
USERNAME_SCHEMA,
passkey,
PASSKEY_SCHEMA,
organization,
ORGANIZATION_SCHEMA,
} from "sonamu/auth";
export default defineConfig({
server: {
auth: {
emailAndPassword: { enabled: true },
plugins: [
admin({ schema: ADMIN_SCHEMA }),
twoFactor({
issuer: "My App",
schema: TWO_FACTOR_SCHEMA,
}),
username({ schema: USERNAME_SCHEMA }),
passkey({ schema: PASSKEY_SCHEMA }),
organization({ schema: ORGANIZATION_SCHEMA }),
],
},
},
});
κ° νλ¬κ·ΈμΈμ μ€ν€λ§(*_SCHEMA)λ Sonamuμ snake_case 컬λΌλͺ
κ³Ό better-authμ camelCase νλλͺ
μ
λ§€ννλ μν μ ν©λλ€. λ°λμ ν΄λΉ νλ¬κ·ΈμΈκ³Ό ν¨κ» μ λ¬ν΄μΌ ν©λλ€.
μ€μ μμ
κΈ°λ³Έ μ€μ
import { defineConfig } from "sonamu";
export default defineConfig({
server: {
auth: {
emailAndPassword: {
enabled: true,
},
},
apiConfig: {
guardHandler: async (guard) => {
const { user } = Sonamu.getContext();
if (guard === "auth" && !user) {
throw new UnauthorizedError("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
},
},
},
});
μμ
λ‘κ·ΈμΈ + μ΄λ©μΌ μΈμ¦
import { defineConfig } from "sonamu";
export default defineConfig({
server: {
auth: {
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
},
},
});
μ£Όμμ¬ν
1. μν°ν° μμ± νμ
# auth μ€μ μ μ λ¨Όμ μν°ν° μμ±
pnpm sonamu better-auth
# λ§μ΄κ·Έλ μ΄μ
μ€ν
pnpm sonamu migrate run
2. νκ²½ λ³μ μ€μ
# .env
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
3. CORS μ€μ
ν΄λΌμ΄μΈνΈκ° λ€λ₯Έ λλ©μΈμμ μ€νλλ κ²½μ°:
export default defineConfig({
server: {
plugins: {
cors: {
origin: ["http://localhost:3000"],
credentials: true,
},
},
auth: {
emailAndPassword: { enabled: true },
},
},
});
4. κΈ°μ‘΄ User μν°ν°μμ νΈνμ±
μ΄λ―Έ User μν°ν°κ° μλ κ²½μ°, pnpm sonamu better-auth μ€ν μ λλ½λ νλλ§ μΆκ°λ©λλ€. κΈ°μ‘΄ λ°μ΄ν°λ μ μ§λ©λλ€.
λ€μ λ¨κ³
μΈμ¦ μ€μ μ μλ£νλ€λ©΄:
- Context - Contextμμ μ¬μ©μ μ 보 μ κ·Ό
- Guards - API μ κ·Ό μ μ΄