๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
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๋กœ ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ

pnpm sonamu better-auth
์ด ๋ช…๋ น์€ ๋‹ค์Œ ์—”ํ‹ฐํ‹ฐ๋“ค์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค:
์—”ํ‹ฐํ‹ฐํ…Œ์ด๋ธ”์„ค๋ช…
Userusers์‚ฌ์šฉ์ž ์ •๋ณด
Sessionsessions์„ธ์…˜ ์ •๋ณด
AccountaccountsOAuth ๊ณ„์ • ์—ฐ๋™ ์ •๋ณด
Verificationverifications์ด๋ฉ”์ผ ์ธ์ฆ ๋“ฑ
์ด๋ฏธ User ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ, ๋ช…๋ น ์‹คํ–‰ ์‹œ ๋ˆ„๋ฝ๋œ ํ•„๋“œ๋งŒ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค.

ํ”Œ๋Ÿฌ๊ทธ์ธ๊ณผ ํ•จ๊ป˜ ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ

better-auth ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด --plugins ์˜ต์…˜์œผ๋กœ ํ•„์š”ํ•œ ํ”Œ๋Ÿฌ๊ทธ์ธ๋“ค์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค:
# ๋‹จ์ผ ํ”Œ๋Ÿฌ๊ทธ์ธ
pnpm sonamu better-auth --plugins=2fa

# ์—ฌ๋Ÿฌ ํ”Œ๋Ÿฌ๊ทธ์ธ (์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„)
pnpm sonamu better-auth --plugins=2fa,admin,username
์ง€์›ํ•˜๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ:
ํ”Œ๋Ÿฌ๊ทธ์ธ ID์„ค๋ช…์ถ”๊ฐ€๋˜๋Š” ํ•„๋“œ/ํ…Œ์ด๋ธ”
2fa2๋‹จ๊ณ„ ์ธ์ฆ (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
passkeyWebAuthn ํŒจ์Šคํ‚ค ์ธ์ฆPasskey ํ…Œ์ด๋ธ”
ssoSSO ๋กœ๊ทธ์ธ (OIDC/SAML)SsoProvider ํ…Œ์ด๋ธ”
api-keyAPI ํ‚ค ์ธ์ฆApiKey ํ…Œ์ด๋ธ”
jwtJWT ํ† ํฐ ๋ฐœ๊ธ‰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"
}

๋กœ๊ทธ์•„์›ƒ

POST /api/auth/sign-out

ํ˜„์žฌ ์„ธ์…˜

GET /api/auth/get-session

์†Œ์…œ ๋กœ๊ทธ์ธ (Google)

GET /api/auth/sign-in/social?provider=google
์ „์ฒด API ๋ชฉ๋ก์€ better-auth ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

Context์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด ์ ‘๊ทผ

์ธ์ฆ๋œ ์š”์ฒญ์—์„œ๋Š” Context๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
import { api, getContext } from "sonamu";

export class MyModel {
  @api()
  static async myApi() {
    const ctx = 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 } = 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 } = 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 ์ ‘๊ทผ ์ œ์–ด