Claude Code로 인증 시스템 구축하기 (JWT & OAuth)
Claude Code로 JWT 듀얼 토큰 인증부터 Google OAuth 연동까지 프로덕션급 인증 시스템을 구축하는 실전 가이드입니다.
인증 시스템 구축에 Claude Code를 쓰는 이유
인증은 보안과 직결되는 기능이라 구현이 어긋나면 곧바로 심각한 취약점으로 이어집니다. Claude Code는 보안 모범 사례를 반영한 코드를 생성해 주고, 놓치기 쉬운 공격 벡터에 대한 대응까지 제안해 줍니다.
JWT 인증 구현
> Express + TypeScript로 JWT 인증을 구현해줘.
> Access Token(15분)과 Refresh Token(7일)을 사용하는 듀얼 토큰 방식으로.
> Refresh Token은 httpOnly 쿠키로 관리하고.
인증 서비스
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import { prisma } from './db';
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET!;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET!;
interface TokenPayload {
userId: string;
email: string;
}
export class AuthService {
async register(email: string, password: string, name: string) {
const existing = await prisma.user.findUnique({ where: { email } });
if (existing) throw new Error('Email already registered');
const hashedPassword = await bcrypt.hash(password, 12);
const user = await prisma.user.create({
data: { email, password: hashedPassword, name },
});
return this.generateTokens({ userId: user.id, email: user.email });
}
async login(email: string, password: string) {
const user = await prisma.user.findUnique({ where: { email } });
if (!user) throw new Error('Invalid credentials');
const valid = await bcrypt.compare(password, user.password);
if (!valid) throw new Error('Invalid credentials');
return this.generateTokens({ userId: user.id, email: user.email });
}
async refreshToken(token: string) {
const payload = jwt.verify(token, REFRESH_TOKEN_SECRET) as TokenPayload;
// DB에서 Refresh Token 유효성 확인
const stored = await prisma.refreshToken.findFirst({
where: { token, userId: payload.userId, revoked: false },
});
if (!stored) throw new Error('Invalid refresh token');
// 기존 토큰을 무효화 (Token Rotation)
await prisma.refreshToken.update({
where: { id: stored.id },
data: { revoked: true },
});
return this.generateTokens(payload);
}
private async generateTokens(payload: TokenPayload) {
const accessToken = jwt.sign(payload, ACCESS_TOKEN_SECRET, {
expiresIn: '15m',
});
const refreshToken = jwt.sign(payload, REFRESH_TOKEN_SECRET, {
expiresIn: '7d',
});
await prisma.refreshToken.create({
data: { token: refreshToken, userId: payload.userId },
});
return { accessToken, refreshToken };
}
}
인증 미들웨어
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = header.slice(7);
try {
const payload = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET!);
req.user = payload as TokenPayload;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
라우터 설정
import { Router } from 'express';
import { AuthService } from './auth-service';
const router = Router();
const auth = new AuthService();
router.post('/register', async (req, res) => {
try {
const { accessToken, refreshToken } = await auth.register(
req.body.email, req.body.password, req.body.name
);
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken });
} catch (err) {
res.status(400).json({ error: (err as Error).message });
}
});
router.post('/login', async (req, res) => {
try {
const { accessToken, refreshToken } = await auth.login(
req.body.email, req.body.password
);
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken });
} catch (err) {
res.status(401).json({ error: 'Invalid credentials' });
}
});
router.post('/refresh', async (req, res) => {
try {
const token = req.cookies.refreshToken;
const { accessToken, refreshToken } = await auth.refreshToken(token);
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken });
} catch (err) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
export default router;
OAuth 연동 (Google)
> PassportJS로 Google 로그인 추가해줘. 기존 JWT 인증 시스템과 통합되게.
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: '/auth/google/callback',
}, async (accessToken, refreshToken, profile, done) => {
let user = await prisma.user.findFirst({
where: { providerId: profile.id, provider: 'google' },
});
if (!user) {
user = await prisma.user.create({
data: {
email: profile.emails![0].value,
name: profile.displayName,
provider: 'google',
providerId: profile.id,
},
});
}
done(null, user);
}));
보안 체크리스트
Claude Code에 다음 프롬프트로 보안 감사를 맡길 수 있습니다.
> 인증 관련 코드를 보안 관점에서 리뷰해줘.
> OWASP Top 10 기준으로 체크해줘.
주요 확인 항목은 다음과 같습니다.
- 비밀번호 해시에 bcrypt를 사용하고 있는지
- JWT 시크릿 키 길이가 충분한지
- Refresh Token Rotation이 구현되어 있는지
- CSRF 대응이 되어 있는지
- Rate Limit이 설정되어 있는지
보안을 포함한 코드 품질 유지에는 리팩터링 자동화도 효과적입니다. 또한 인증 관련 설정 방침을 CLAUDE.md에 기록해 두면 Claude Code가 일관된 코드를 생성해 줍니다.
정리
Claude Code를 활용하면 JWT 인증과 OAuth 연동을 포함한 견고한 인증 시스템을 효율적으로 구축할 수 있습니다. 보안 모범 사례에 기반한 코드가 생성되기 때문에 놓치기 쉬운 취약점에도 대처하기 쉬워지죠. 다만 프로덕션 환경에서는 시크릿 키의 안전한 관리와 HTTPS 통신을 반드시 지켜 주세요.
자세한 내용은 Anthropic 공식 문서를 확인해 보세요.
Claude Code 워크플로우를 한 단계 업그레이드하세요
지금 바로 Claude Code에 복사해 쓸 수 있는 검증된 프롬프트 템플릿 50선.
이 글을 작성한 사람
Masa
Claude Code를 적극 활용하는 엔지니어. 10개 언어, 2,000페이지 이상의 테크 미디어 claudecode-lab.com을 운영 중.
관련 글
Claude Code로 리팩토링을 자동화하는 방법
Claude Code를 활용해 코드 리팩토링을 효율적으로 자동화하는 방법을 알아봅니다. 실전 프롬프트와 구체적인 리팩토링 패턴을 소개합니다.
Claude Code로 사이드 프로젝트 개발 속도를 극대화하는 방법 [예제 포함]
Claude Code를 활용해 개인 프로젝트 개발 속도를 획기적으로 높이는 방법을 알아봅니다. 실전 예제와 아이디어부터 배포까지의 워크플로를 포함합니다.
Complete CORS Configuration Guide: Claude Code 활용 가이드
complete cors configuration guide: Claude Code 활용. 실용적인 팁과 코드 예시를 포함합니다.