인증
이 가이드에서는 인증 패턴을 다루고 TanStack Start로 자체 인증 시스템을 구현하는 방법을 보여 줍니다.
📋 시작하기 전에: 파트너 솔루션과 호스팅 서비스를 포함한 모든 옵션은 인증 개요에서 확인합니다.
인증 접근 방식
TanStack Start 애플리케이션의 인증에는 여러 가지 옵션이 있습니다:
호스팅 솔루션:
- Clerk - UI 컴포넌트를 갖춘 완전한 인증 플랫폼
- WorkOS - SSO 및 규정 준수 기능을 갖춘 엔터프라이즈 중심 솔루션
- Better Auth - 오픈 소스 TypeScript 라이브러리
- Auth.js - 80개 이상의 OAuth 제공자를 지원하는 오픈 소스 라이브러리
직접 구현의 이점:
- 완전한 제어: 인증 흐름을 완전히 맞춤 설정할 수 있습니다
- 특정 공급업체에 종속되지 않음: 인증 로직과 사용자 데이터를 직접 소유합니다
- 맞춤형 요구 사항: 특정 비즈니스 로직이나 규정 준수 요구 사항을 구현합니다
- 비용 제어: 사용자별 요금이나 사용량 제한이 없습니다
인증에는 비밀번호 보안, 세션 관리, 속도 제한, CSRF 보호 및 다양한 공격 벡터를 비롯한 많은 고려 사항이 포함됩니다.
핵심 개념
인증과 권한 부여
- 인증: 이 사용자는 누구입니까? (로그인/로그아웃)
- 권한 부여: 이 사용자는 무엇을 할 수 있습니까? (권한/역할)
TanStack Start는 서버 함수, 세션 및 라우트 보호를 통해 두 가지 모두에 필요한 도구를 제공합니다.
데이터/API 경계를 먼저 보호합니다. 비공개 데이터를 반환하거나 변경하는 모든 서버 함수, 서버 라우트 또는 기타 API 엔드포인트는 요청 자체에 권한을 부여해야 합니다.
beforeLoad은 라우트 UX에 유용합니다. 사용자가 이용할 수 없는 화면에 접근하지 못하게 하고 어차피 실패할 작업이 실행되는 것을 방지합니다. 이는 데이터의 보안 경계가 아닙니다. 서버 측 패턴은 인증 서버 프리미티브를 참조합니다.
필수 구성 요소
1. 인증을 위한 서버 함수
서버 함수는 민감한 인증 로직을 서버에서 안전하게 처리합니다:
import { createServerFn } from '@tanstack/solid-start'
import { redirect } from '@tanstack/solid-router'
// Login server function
export const loginFn = createServerFn({ method: 'POST' })
.validator((data: { email: string; password: string }) => data)
.handler(async ({ data }) => {
// Verify credentials (replace with your auth logic)
const user = await authenticateUser(data.email, data.password)
if (!user) {
return { error: 'Invalid credentials' }
}
// Create session
const session = await useAppSession()
await session.update({
userId: user.id,
email: user.email,
})
// Redirect to protected area
throw redirect({ to: '/dashboard' })
})
// Logout server function
export const logoutFn = createServerFn({ method: 'POST' }).handler(async () => {
const session = await useAppSession()
await session.clear()
throw redirect({ to: '/' })
})
// Get current user
export const getCurrentUserFn = createServerFn({ method: 'GET' }).handler(
async () => {
const session = await useAppSession()
const userId = session.get('userId')
if (!userId) {
return null
}
return await getUserById(userId)
},
)
2. 세션 관리
TanStack Start는 안전한 HTTP-only 쿠키 세션을 제공합니다:
// utils/session.ts
import { useSession } from '@tanstack/solid-start/server'
type SessionData = {
userId?: string
email?: string
role?: string
}
export function useAppSession() {
return useSession<SessionData>({
// Session configuration
name: 'app-session',
password: process.env.SESSION_SECRET!, // At least 32 characters
// Optional: customize cookie settings
cookie: {
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
httpOnly: true,
},
})
}
3. 인증 컨텍스트
애플리케이션 전체에서 인증 상태를 공유합니다:
// contexts/auth.tsx
import { createContext, useContext } from 'solid-js'
import { useServerFn } from '@tanstack/solid-start'
import { getCurrentUserFn } from '../server/auth'
type User = {
id: string
email: string
role: string
}
type AuthContextType = {
user: User | null
isLoading: boolean
refetch: () => void
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export function AuthProvider(props) {
const { data: user, isLoading, refetch } = useServerFn(getCurrentUserFn)
return (
<AuthContext.Provider value={{ user, isLoading, refetch }}>
{props.children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}
4. 라우트 보호
beforeLoad을 사용하여 라우트를 보호합니다:
// routes/_authed.tsx - Layout route for protected pages
import { createFileRoute, redirect } from '@tanstack/solid-router'
import { getCurrentUserFn } from '../server/auth'
export const Route = createFileRoute('/_authed')({
beforeLoad: async ({ location }) => {
const user = await getCurrentUserFn()
if (!user) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
// Pass user to child routes
return { user }
},
})
// routes/_authed/dashboard.tsx - Protected route
import { createFileRoute } from '@tanstack/solid-router'
export const Route = createFileRoute('/_authed/dashboard')({
component: DashboardComponent,
})
function DashboardComponent() {
const context = Route.useRouteContext()
return (
<div>
<h1>Welcome, {context().user.email}!</h1>
{/* Dashboard content */}
</div>
)
}
구현 패턴
기본 이메일/비밀번호 인증
// server/auth.ts
import bcrypt from 'bcryptjs'
import { createServerFn } from '@tanstack/solid-start'
// User registration
export const registerFn = createServerFn({ method: 'POST' })
.validator((data: { email: string; password: string; name: string }) => data)
.handler(async ({ data }) => {
// Check if user exists
const existingUser = await getUserByEmail(data.email)
if (existingUser) {
return { error: 'User already exists' }
}
// Hash password
const hashedPassword = await bcrypt.hash(data.password, 12)
// Create user
const user = await createUser({
email: data.email,
password: hashedPassword,
name: data.name,
})
// Create session
const session = await useAppSession()
await session.update({ userId: user.id })
return { success: true, user: { id: user.id, email: user.email } }
})
async function authenticateUser(email: string, password: string) {
const user = await getUserByEmail(email)
if (!user) return null
const isValid = await bcrypt.compare(password, user.password)
return isValid ? user : null
}
역할 기반 접근 제어(RBAC)
// utils/auth.ts
export const roles = {
USER: 'user',
ADMIN: 'admin',
MODERATOR: 'moderator',
} as const
type Role = (typeof roles)[keyof typeof roles]
export function hasPermission(userRole: Role, requiredRole: Role): boolean {
const hierarchy = {
[roles.USER]: 0,
[roles.MODERATOR]: 1,
[roles.ADMIN]: 2,
}
return hierarchy[userRole] >= hierarchy[requiredRole]
}
// Protected route with role check
export const Route = createFileRoute('/_authed/admin/')({
beforeLoad: async ({ context }) => {
if (!hasPermission(context.user.role, roles.ADMIN)) {
throw redirect({ to: '/unauthorized' })
}
},
})
소셜 인증 통합
// Example with OAuth providers
export const authProviders = {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
redirectUri: `${process.env.APP_URL}/auth/google/callback`,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
redirectUri: `${process.env.APP_URL}/auth/github/callback`,
},
}
export const initiateOAuthFn = createServerFn({ method: 'POST' })
.validator((data: { provider: 'google' | 'github' }) => data)
.handler(async ({ data }) => {
const provider = authProviders[data.provider]
const state = generateRandomState()
// Store state in session for CSRF protection
const session = await useAppSession()
await session.update({ oauthState: state })
// Generate OAuth URL
const authUrl = generateOAuthUrl(provider, state)
throw redirect({ href: authUrl })
})
비밀번호 재설정 흐름
// Password reset request
export const requestPasswordResetFn = createServerFn({ method: 'POST' })
.validator((data: { email: string }) => data)
.handler(async ({ data }) => {
const user = await getUserByEmail(data.email)
if (!user) {
// Don't reveal if email exists
return { success: true }
}
const token = generateSecureToken()
const expires = new Date(Date.now() + 60 * 60 * 1000) // 1 hour
await savePasswordResetToken(user.id, token, expires)
await sendPasswordResetEmail(user.email, token)
return { success: true }
})
// Password reset confirmation
export const resetPasswordFn = createServerFn({ method: 'POST' })
.validator((data: { token: string; newPassword: string }) => data)
.handler(async ({ data }) => {
const resetToken = await getPasswordResetToken(data.token)
if (!resetToken || resetToken.expires < new Date()) {
return { error: 'Invalid or expired token' }
}
const hashedPassword = await bcrypt.hash(data.newPassword, 12)
await updateUserPassword(resetToken.userId, hashedPassword)
await deletePasswordResetToken(data.token)
return { success: true }
})
보안 모범 사례
1. 비밀번호 보안
// Use strong hashing (bcrypt, scrypt, or argon2)
import bcrypt from 'bcryptjs'
const saltRounds = 12 // Adjust based on your security needs
const hashedPassword = await bcrypt.hash(password, saltRounds)
2. 세션 보안
// Use secure session configuration
export function useAppSession() {
return useSession({
name: 'app-session',
password: process.env.SESSION_SECRET!, // 32+ characters
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
sameSite: 'lax', // CSRF protection
httpOnly: true, // XSS protection
maxAge: 7 * 24 * 60 * 60, // 7 days
},
})
}
3. 요청 속도 제한
// Simple in-memory rate limiting (use Redis in production)
const loginAttempts = new Map<string, { count: number; resetTime: number }>()
export const rateLimitLogin = (ip: string): boolean => {
const now = Date.now()
const attempts = loginAttempts.get(ip)
if (!attempts || now > attempts.resetTime) {
loginAttempts.set(ip, { count: 1, resetTime: now + 15 * 60 * 1000 }) // 15 min
return true
}
if (attempts.count >= 5) {
return false // Too many attempts
}
attempts.count++
return true
}
4. 입력 유효성 검사
import { z } from 'zod'
const loginSchema = z.object({
email: z.string().email().max(255),
password: z.string().min(8).max(100),
})
export const loginFn = createServerFn({ method: 'POST' })
.validator((data) => loginSchema.parse(data))
.handler(async ({ data }) => {
// data is now validated
})
인증 테스트
서버 함수 단위 테스트
// __tests__/auth.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { loginFn } from '../server/auth'
describe('Authentication', () => {
beforeEach(async () => {
await setupTestDatabase()
})
it('should login with valid credentials', async () => {
const result = await loginFn({
data: { email: 'test@example.com', password: 'password123' },
})
expect(result.error).toBeUndefined()
expect(result.user).toBeDefined()
})
it('should reject invalid credentials', async () => {
const result = await loginFn({
data: { email: 'test@example.com', password: 'wrongpassword' },
})
expect(result.error).toBe('Invalid credentials')
})
})
통합 테스트
// __tests__/auth-flow.test.tsx
import { render, screen, fireEvent, waitFor } from '@solidjs/testing-library'
import { RouterProvider, createMemoryHistory } from '@tanstack/solid-router'
import { router } from '../router'
describe('Authentication Flow', () => {
it('should redirect to login when accessing protected route', async () => {
const history = createMemoryHistory()
history.push('/dashboard') // Protected route
render(<RouterProvider router={router} history={history} />)
await waitFor(() => {
expect(screen.getByText('Login')).toBeInTheDocument()
})
})
})
일반적인 패턴
로딩 상태
function LoginForm() {
const [isLoading, setIsLoading] = createSignal(false)
const loginMutation = useServerFn(loginFn)
const handleSubmit = async (data: LoginData) => {
setIsLoading(true)
try {
await loginMutation.mutate(data)
} catch (error) {
// Handle error
} finally {
setIsLoading(false)
}
}
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button disabled={isLoading()}>
{isLoading() ? 'Logging in...' : 'Login'}
</button>
</form>
)
}
로그인 상태 유지 기능
export const loginFn = createServerFn({ method: 'POST' })
.validator(
(data: { email: string; password: string; rememberMe?: boolean }) => data,
)
.handler(async ({ data }) => {
const user = await authenticateUser(data.email, data.password)
if (!user) return { error: 'Invalid credentials' }
const session = await useAppSession()
await session.update(
{ userId: user.id },
{
// Extend session if remember me is checked
maxAge: data.rememberMe ? 30 * 24 * 60 * 60 : undefined, // 30 days vs session
},
)
return { success: true }
})
다른 솔루션에서 마이그레이션
클라이언트 측 인증에서 마이그레이션
클라이언트 측 인증(localStorage, context만 사용)에서 마이그레이션하는 경우:
- 인증 로직을 서버 함수로 이동합니다
- localStorage를 서버 세션으로 대체합니다
- 라우트 보호에서
beforeLoad을 사용하도록 업데이트합니다 - 적절한 보안 헤더와 CSRF 보호를 추가합니다
다른 프레임워크에서 마이그레이션
- Next.js: API 라우트를 서버 함수로 대체하고 NextAuth 세션을 마이그레이션합니다
- Remix: 로더/액션을 서버 함수로 변환하고 세션 패턴을 조정합니다
- SvelteKit: 폼 액션을 서버 함수로 이동하고 라우트 보호를 업데이트합니다
프로덕션 고려 사항
인증 방식을 선택할 때 다음 요소를 고려합니다:
호스팅형과 직접 구현 비교
호스팅형 솔루션(Clerk, WorkOS, Better Auth):
- 사전 구축된 보안 조치와 정기 업데이트
- UI 컴포넌트와 사용자 관리 기능
- 규정 준수 인증과 감사 추적 기록
- 지원 및 문서
- 사용자별 또는 구독형 요금제
직접 구현:
- 구현과 데이터를 완전히 제어할 수 있습니다
- 지속적인 구독 비용이 없습니다
- 사용자 지정 비즈니스 로직과 워크플로를 구현할 수 있습니다
- 보안 업데이트와 모니터링을 직접 책임져야 합니다
- 예외 상황과 공격 벡터를 처리해야 합니다
보안 고려 사항
인증 시스템은 다양한 보안 측면을 처리해야 합니다:
- 비밀번호 해싱과 타이밍 공격 방지
- 세션 관리와 세션 고정 공격 방지
- CSRF 및 XSS 보호
- 요청 속도 제한과 무차별 대입 공격 방지
- OAuth 흐름 보안
- 규정 준수 요구 사항(GDPR, CCPA 등)
다음 단계
인증을 구현할 때 다음 사항을 고려합니다:
- 보안 검토: 보안 모범 사례에 따라 구현을 검토합니다
- 성능: 사용자 조회 및 세션 검증에 캐시를 추가합니다
- 모니터링: 인증 이벤트에 로깅 및 모니터링을 추가합니다
- 규정 준수: 개인 데이터를 저장하는 경우 관련 규정을 준수해야 합니다
다른 인증 접근 방식은 인증 개요를 확인합니다. 구체적인 통합 지원은 방법 가이드를 참조하거나 실제 작동하는 예제를 살펴봅니다.