Use Cases

How to Build an Authentication System with Claude Code (JWT and OAuth)

How to Build an Authentication System: Claude Code 활용 (JWT and OAuth). 코드 예시가 포함된 실전 가이드입니다.

인증システム구축にClaude Code를 사용하는メリット

인증は보안に直結する機能であり、구현ミスは重大な脆弱性につながります。Claude Code는 보안の모범 사례を踏まえた구현を생성でき、見落としがちな攻撃ベクトルへの対策も提案してくれます。

JWT인증の구현

> Express + TypeScriptでJWT인증를 구현해줘。
> アクセス토큰(15分)とリフレッシュ토큰(7日)の2토큰方式で。
> リフレッシュ토큰は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でリフレッシュ토큰の유효性を확인
    const stored = await prisma.refreshToken.findFirst({
      where: { token, userId: payload.userId, revoked: false },
    });
    if (!stored) throw new Error('Invalid refresh token');

    // 古い토큰を무효化(토큰ローテーション)
    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のシークレットキーが十分に長いか
  • リフレッシュ토큰のローテーションが구현されているか
  • CSRF対策が施されているか
  • レートリミットが설정されているか

보안を含むコード品質の維持には리팩터링の자동화も効果的です。또한、인증の설정方針をCLAUDE.mdに記述しておくと、Claude Code가 一貫したコードを생성します。

정리

Claude Code를 활용하면 JWT인증やOAuth연동を含む堅牢な인증システムを효율적으로구축할 수 있습니다。보안の모범 사례を踏まえたコードが생성されるため、見落としがちな脆弱性にも対処しやすくなります。必ず프로덕션 환경ではシークレットキーの安全な관리とHTTPS通信を徹底して주세요。

자세한 내용은Anthropic공식 문서를 확인하세요.

#Claude Code #authentication #JWT #OAuth #security