authentication-setup
npx skills add https://github.com/supercent-io/skills-template --skill authentication-setup
Agent 安装分布
Skill 文档
Authentication Setup
When to use this skill
ì´ ì¤í¬ì í¸ë¦¬ê±°í´ì¼ íë 구체ì ì¸ ìí©ì ëì´í©ëë¤:
- ì¬ì©ì ë¡ê·¸ì¸ ìì¤í : ìë¡ì´ ì í리ì¼ì´ì ì ì¬ì©ì ì¸ì¦ 기ë¥ì ì¶ê°í ë
- API ë³´ì: REST APIë GraphQL APIì ì¸ì¦ ë ì´ì´ë¥¼ ì¶ê°í ë
- ê¶í ê´ë¦¬: ì¬ì©ì ìí ì ë°ë¥¸ ì ê·¼ ì ì´ê° íìí ë
- ì¸ì¦ ë§ì´ê·¸ë ì´ì : 기존 ì¸ì¦ ìì¤í ì JWTë OAuthë¡ ì íí ë
- SSO íµí©: Google, GitHub, Microsoft ë±ì ìì ë¡ê·¸ì¸ì íµí©í ë
ì ë ¥ íì (Input Format)
ì¬ì©ìë¡ë¶í° ë°ìì¼ í ì ë ¥ì íìê³¼ íì/ì í ì ë³´:
íì ì ë³´
- ì¸ì¦ ë°©ì: JWT, Session, OAuth 2.0 ì¤ ì í
- ë°±ìë íë ììí¬: Express, Django, FastAPI, Spring Boot ë±
- ë°ì´í°ë² ì´ì¤: PostgreSQL, MySQL, MongoDB ë±
- ë³´ì ì구ì¬í: ë¹ë°ë²í¸ ì ì± , í í° ë§ë£ ìê° ë±
ì í ì ë³´
- MFA ì§ì: 2FA/MFA íì±í ì¬ë¶ (기본ê°: false)
- ìì ë¡ê·¸ì¸: OAuth ì ê³µì (Google, GitHub, etc.)
- ì¸ì ì ì¥ì: Redis, in-memory ë± (Session ë°©ìì¸ ê²½ì°)
- Refresh Token: ì¬ì© ì¬ë¶ (기본ê°: true)
ì ë ¥ ìì
ì¬ì©ì ì¸ì¦ ìì¤í
ì 구ì¶í´ì¤:
- ì¸ì¦ ë°©ì: JWT
- íë ììí¬: Express.js + TypeScript
- ë°ì´í°ë² ì´ì¤: PostgreSQL
- MFA: Google Authenticator ì§ì
- ìì
ë¡ê·¸ì¸: Google, GitHub
- Refresh Token: ì¬ì©
Instructions
ë¨ê³ë³ë¡ ì ííê² ë°ë¼ì¼ í ìì ìì를 ëª ìí©ëë¤.
Step 1: ë°ì´í° ëª¨ë¸ ì¤ê³
ì¬ì©ì ë° ì¸ì¦ ê´ë ¨ ë°ì´í°ë² ì´ì¤ ì¤í¤ë§ë¥¼ ì¤ê³í©ëë¤.
ìì ë´ì©:
- User í ì´ë¸ ì¤ê³ (id, email, password_hash, role, created_at, updated_at)
- RefreshToken í ì´ë¸ (ì íì¬í)
- OAuthProvider í ì´ë¸ (ìì ë¡ê·¸ì¸ ì¬ì©ì)
- ë¹ë°ë²í¸ë ì ë í문 ì ì¥íì§ ìì (bcrypt/argon2 í´ì± íì)
ìì (PostgreSQL):
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255), -- NULL if OAuth only
role VARCHAR(50) DEFAULT 'user',
is_verified BOOLEAN DEFAULT false,
mfa_secret VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
token VARCHAR(500) UNIQUE NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
Step 2: ë¹ë°ë²í¸ ë³´ì 구í
ë¹ë°ë²í¸ í´ì± ë° ê²ì¦ ë¡ì§ì 구íí©ëë¤.
ìì ë´ì©:
- bcrypt (Node.js) ëë argon2 (Python) ì¬ì©
- Salt rounds ìµì 10 ì´ì ì¤ì
- ë¹ë°ë²í¸ ê°ë ê²ì¦ (ìµì 8ì, ëì문ì, ì«ì, í¹ì문ì)
íë¨ ê¸°ì¤:
- Node.js íë¡ì í¸ â bcrypt ë¼ì´ë¸ë¬ë¦¬ ì¬ì©
- Python íë¡ì í¸ â argon2-cffi ëë passlib ì¬ì©
- ì±ë¥ì´ ì¤ìí ê²½ì° â bcrypt ì í
- ìµê³ ë³´ìì´ íìí ê²½ì° â argon2 ì í
ìì (Node.js + TypeScript):
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
export async function hashPassword(password: string): Promise<string> {
// ë¹ë°ë²í¸ ê°ë ê²ì¦
if (password.length < 8) {
throw new Error('Password must be at least 8 characters');
}
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password);
if (!hasUpperCase || !hasLowerCase || !hasNumber || !hasSpecial) {
throw new Error('Password must contain uppercase, lowercase, number, and special character');
}
return await bcrypt.hash(password, SALT_ROUNDS);
}
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return await bcrypt.compare(password, hash);
}
Step 3: JWT í í° ìì± ë° ê²ì¦
JWT ê¸°ë° ì¸ì¦ì ìí í í° ìì¤í ì 구íí©ëë¤.
ìì ë´ì©:
- Access Token (ì§§ì ë§ë£ ìê°: 15ë¶)
- Refresh Token (긴 ë§ë£ ìê°: 7ì¼~30ì¼)
- JWT ìëª ì ê°ë ¥í SECRET í¤ ì¬ì© (íê²½ë³ìë¡ ê´ë¦¬)
- í í° íì´ë¡ëì ìµì ì ë³´ë§ í¬í¨ (user_id, role)
ìì (Node.js):
import jwt from 'jsonwebtoken';
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET!;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET!;
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
interface TokenPayload {
userId: string;
email: string;
role: string;
}
export function generateAccessToken(payload: TokenPayload): string {
return jwt.sign(payload, ACCESS_TOKEN_SECRET, {
expiresIn: ACCESS_TOKEN_EXPIRY,
issuer: 'your-app-name',
audience: 'your-app-users'
});
}
export function generateRefreshToken(payload: TokenPayload): string {
return jwt.sign(payload, REFRESH_TOKEN_SECRET, {
expiresIn: REFRESH_TOKEN_EXPIRY,
issuer: 'your-app-name',
audience: 'your-app-users'
});
}
export function verifyAccessToken(token: string): TokenPayload {
return jwt.verify(token, ACCESS_TOKEN_SECRET, {
issuer: 'your-app-name',
audience: 'your-app-users'
}) as TokenPayload;
}
export function verifyRefreshToken(token: string): TokenPayload {
return jwt.verify(token, REFRESH_TOKEN_SECRET, {
issuer: 'your-app-name',
audience: 'your-app-users'
}) as TokenPayload;
}
Step 4: ì¸ì¦ 미ë¤ì¨ì´ 구í
API ìì²ì ë³´í¸íë ì¸ì¦ 미ë¤ì¨ì´ë¥¼ ìì±í©ëë¤.
íì¸ ì¬í:
- Authorization í¤ëìì Bearer í í° ì¶ì¶
- í í° ê²ì¦ ë° ë§ë£ íì¸
- ì í¨í í í°ì¸ ê²½ì° req.userì ì¬ì©ì ì ë³´ ì¶ê°
- ìë¬ ì²ë¦¬ (401 Unauthorized)
ìì (Express.js):
import { Request, Response, NextFunction } from 'express';
import { verifyAccessToken } from './jwt';
export interface AuthRequest extends Request {
user?: {
userId: string;
email: string;
role: string;
};
}
export function authenticateToken(req: AuthRequest, res: Response, next: NextFunction) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
const payload = verifyAccessToken(token);
req.user = payload;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(403).json({ error: 'Invalid token' });
}
}
// Role-based authorization middleware
export function requireRole(...roles: string[]) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
Step 5: ì¸ì¦ API ìëí¬ì¸í¸ 구í
íìê°ì , ë¡ê·¸ì¸, í í° ê°±ì ë±ì API를 ìì±í©ëë¤.
ìì ë´ì©:
- POST /auth/register – íìê°ì
- POST /auth/login – ë¡ê·¸ì¸
- POST /auth/refresh – í í° ê°±ì
- POST /auth/logout – ë¡ê·¸ìì
- GET /auth/me – íì¬ ì¬ì©ì ì ë³´
ìì:
import express from 'express';
import { hashPassword, verifyPassword } from './password';
import { generateAccessToken, generateRefreshToken, verifyRefreshToken } from './jwt';
import { authenticateToken } from './middleware';
const router = express.Router();
// íìê°ì
router.post('/register', async (req, res) => {
try {
const { email, password } = req.body;
// ì´ë©ì¼ ì¤ë³µ íì¸
const existingUser = await db.user.findUnique({ where: { email } });
if (existingUser) {
return res.status(409).json({ error: 'Email already exists' });
}
// ë¹ë°ë²í¸ í´ì±
const passwordHash = await hashPassword(password);
// ì¬ì©ì ìì±
const user = await db.user.create({
data: { email, password_hash: passwordHash, role: 'user' }
});
// í í° ìì±
const accessToken = generateAccessToken({
userId: user.id,
email: user.email,
role: user.role
});
const refreshToken = generateRefreshToken({
userId: user.id,
email: user.email,
role: user.role
});
// Refresh token DB ì ì¥
await db.refreshToken.create({
data: {
user_id: user.id,
token: refreshToken,
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7ì¼
}
});
res.status(201).json({
user: { id: user.id, email: user.email, role: user.role },
accessToken,
refreshToken
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ë¡ê·¸ì¸
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// ì¬ì©ì 찾기
const user = await db.user.findUnique({ where: { email } });
if (!user || !user.password_hash) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// ë¹ë°ë²í¸ íì¸
const isValid = await verifyPassword(password, user.password_hash);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// í í° ìì±
const accessToken = generateAccessToken({
userId: user.id,
email: user.email,
role: user.role
});
const refreshToken = generateRefreshToken({
userId: user.id,
email: user.email,
role: user.role
});
// Refresh token ì ì¥
await db.refreshToken.create({
data: {
user_id: user.id,
token: refreshToken,
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
}
});
res.json({
user: { id: user.id, email: user.email, role: user.role },
accessToken,
refreshToken
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// í í° ê°±ì
router.post('/refresh', async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
// Refresh token ê²ì¦
const payload = verifyRefreshToken(refreshToken);
// DBìì í í° íì¸
const storedToken = await db.refreshToken.findUnique({
where: { token: refreshToken }
});
if (!storedToken || storedToken.expires_at < new Date()) {
return res.status(403).json({ error: 'Invalid or expired refresh token' });
}
// ì Access token ìì±
const accessToken = generateAccessToken({
userId: payload.userId,
email: payload.email,
role: payload.role
});
res.json({ accessToken });
} catch (error) {
res.status(403).json({ error: 'Invalid refresh token' });
}
});
// íì¬ ì¬ì©ì ì ë³´
router.get('/me', authenticateToken, async (req: AuthRequest, res) => {
try {
const user = await db.user.findUnique({
where: { id: req.user!.userId },
select: { id: true, email: true, role: true, created_at: true }
});
res.json({ user });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
export default router;
Output format
ê²°ê³¼ë¬¼ì´ ë°ë¼ì¼ í ì íí íìì ì ìí©ëë¤.
기본 구조
íë¡ì í¸ ëë í 리/
âââ src/
â âââ auth/
â â âââ password.ts # ë¹ë°ë²í¸ í´ì±/ê²ì¦
â â âââ jwt.ts # JWT í í° ìì±/ê²ì¦
â â âââ middleware.ts # ì¸ì¦ 미ë¤ì¨ì´
â â âââ routes.ts # ì¸ì¦ API ìëí¬ì¸í¸
â âââ models/
â â âââ User.ts # ì¬ì©ì 모ë¸
â âââ database/
â âââ schema.sql # ë°ì´í°ë² ì´ì¤ ì¤í¤ë§
âââ .env.example # íê²½ë³ì í
í릿
âââ README.md # ì¸ì¦ ìì¤í
문ì
íê²½ë³ì íì¼ (.env.example)
# JWT Secrets (MUST change in production)
ACCESS_TOKEN_SECRET=your-access-token-secret-min-32-characters
REFRESH_TOKEN_SECRET=your-refresh-token-secret-min-32-characters
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
# OAuth (Optional)
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
Constraints
ë°ëì ì§ì¼ì¼ í ê·ì¹ê³¼ ê¸ì§ ì¬íì ëª ìí©ëë¤.
íì ê·ì¹ (MUST)
-
ë¹ë°ë²í¸ ë³´ì: ì ë í문ì¼ë¡ ì ì¥íì§ ìì
- bcrypt, argon2 ë± ê²ì¦ë í´ì± ìê³ ë¦¬ì¦ ì¬ì©
- Salt rounds ìµì 10 ì´ì
-
íê²½ë³ì ê´ë¦¬: 모ë ìí¬ë¦¿ í¤ë íê²½ë³ìë¡ ê´ë¦¬
- .env íì¼ì .gitignoreì ì¶ê°
- .env.exampleë¡ íìí ë³ì ëª©ë¡ ì ê³µ
-
í í° ë§ë£: Access Tokenì ì§§ê² (15ë¶), Refresh Tokenì ì ì í (7ì¼)
- ë³´ìê³¼ UXì ê· í ê³ ë ¤
- Refresh Tokenì DBì ì ì¥íì¬ ë¬´í¨í ê°ë¥íê²
ê¸ì§ ì¬í (MUST NOT)
-
í문 ë¹ë°ë²í¸: ì ë ë¹ë°ë²í¸ë¥¼ í문ì¼ë¡ ì ì¥íê±°ë ë¡ê·¸ì ì¶ë ¥íì§ ìì
- ì¬ê°í ë³´ì ìí
- ë²ì ì± ì 문ì
-
JWT SECRET íëì½ë©: ì½ëì SECRET í¤ë¥¼ ì§ì ìì±íì§ ìì
- GitHubì ë ¸ì¶ë ìí
- íë¡ëì ë³´ì ì·¨ì½ì
-
민ê°ì ë³´ í í° í¬í¨: JWT íì´ë¡ëì ë¹ë°ë²í¸, ì¹´ëë²í¸ ë± ë¯¼ê°ì ë³´ í¬í¨ ê¸ì§
- JWTë ëì½ë© ê°ë¥ (ìí¸í ìë)
- ìµìíì ì ë³´ë§ í¬í¨ (user_id, role)
ë³´ì ê·ì¹
- Rate Limiting: ë¡ê·¸ì¸ APIì rate limiting ì ì© (brute force ë°©ì§)
- HTTPS íì: íë¡ëì íê²½ììë HTTPSë§ ì¬ì©
- CORS ì¤ì : íì©ë ëë©ì¸ë§ API ì ê·¼ ê°ë¥íëë¡ ì¤ì
- Input Validation: 모ë ì¬ì©ì ì ë ¥ ê²ì¦ (SQL Injection, XSS ë°©ì§)
Examples
ì¤ì ì¬ì© ì¬ë¡ë¥¼ íµí´ ì¤í¬ì ì ì© ë°©ë²ì ë³´ì¬ì¤ëë¤.
ìì 1: Express.js + PostgreSQL JWT ì¸ì¦
ìí©: Node.js Express ì±ì JWT ê¸°ë° ì¬ì©ì ì¸ì¦ ì¶ê°
ì¬ì©ì ìì²:
Express.js ì±ì JWT ì¸ì¦ì ì¶ê°í´ì¤. PostgreSQL ì¬ì©íê³ ,
access tokenì 15ë¶, refresh tokenì 7ì¼ë¡ ì¤ì í´ì¤.
ì¤í¬ ì ì© ê³¼ì :
-
í¨í¤ì§ ì¤ì¹:
npm install jsonwebtoken bcrypt pg npm install --save-dev @types/jsonwebtoken @types/bcrypt -
ë°ì´í°ë² ì´ì¤ ì¤í¤ë§ ìì± (ìì SQL ì¬ì©)
-
íê²½ë³ì ì¤ì :
ACCESS_TOKEN_SECRET=$(openssl rand -base64 32) REFRESH_TOKEN_SECRET=$(openssl rand -base64 32) -
ì¸ì¦ 모ë 구í (ìì ì½ë ìì ì¬ì©)
-
API ë¼ì°í¸ ì°ê²°:
import authRoutes from './auth/routes'; app.use('/api/auth', authRoutes);
ìµì¢ ê²°ê³¼: JWT ê¸°ë° ì¸ì¦ ìì¤í ìì±, íìê°ì /ë¡ê·¸ì¸/í í°ê°±ì API ëì
ìì 2: Role-Based Access Control (RBAC)
ìí©: ê´ë¦¬ìì ì¼ë° ì¬ì©ì를 구ë¶íë ê¶í ìì¤í
ì¬ì©ì ìì²:
ê´ë¦¬ìë§ ì ê·¼ ê°ë¥í API를 ë§ë¤ì´ì¤.
ì¼ë° ì¬ì©ìë 403 ìë¬ë¥¼ ë°ìì¼ í´.
ìµì¢ ê²°ê³¼:
// ê´ë¦¬ì ì ì© API
router.delete('/users/:id',
authenticateToken, // ì¸ì¦ íì¸
requireRole('admin'), // ìí íì¸
async (req, res) => {
// ì¬ì©ì ìì ë¡ì§
await db.user.delete({ where: { id: req.params.id } });
res.json({ message: 'User deleted' });
}
);
// ì¬ì© ìì
// ì¼ë° ì¬ì©ì(role: 'user') ìì² â 403 Forbidden
// ê´ë¦¬ì(role: 'admin') ìì² â 200 OK
Best practices
í¨ê³¼ì ì¼ë¡ ì´ ì¤í¬ì ì¬ì©í기 ìí ê¶ì¥ì¬íì ëë¤.
íì§ í¥ì
-
Password Rotation Policy: 주기ì ì¸ ë¹ë°ë²í¸ ë³ê²½ ê¶ì¥
- 90ì¼ë§ë¤ ë³ê²½ ì림
- ì´ì 5ê° ë¹ë°ë²í¸ ì¬ì¬ì© ë°©ì§
- ì¬ì©ì ê²½íê³¼ ë³´ìì ê· í
-
Multi-Factor Authentication (MFA): ì¤ì ê³ì ì 2FA ì ì©
- Google Authenticator, Authy ë± TOTP ì± ì¬ì©
- SMSë ë³´ìì± ë®ì (SIM swapping ìí)
- Backup codes ì ê³µ
-
Audit Logging: 모ë ì¸ì¦ ì´ë²¤í¸ ë¡ê¹
- ë¡ê·¸ì¸ ì±ê³µ/ì¤í¨, IP 주ì, User Agent 기ë¡
- ì´ì íì§ ë° ì¬í ë¶ì
- GDPR ì¤ì (민ê°ì ë³´ ì ì¸)
í¨ì¨ì± ê°ì
- Token Blacklist: ë¡ê·¸ìì ì Refresh Token 무í¨í
- Redis Caching: ì주 ì¬ì©íë ì¬ì©ì ì ë³´ ìºì±
- Database Indexing: email, refresh_tokenì ì¸ë±ì¤ ì¶ê°
ì주 ë°ìíë 문ì (Common Issues)
íí ë°ìíë 문ì ì í´ê²° ë°©ë²ì ëë¤.
문ì 1: “JsonWebTokenError: invalid signature”
ì¦ì:
- í í° ê²ì¦ ì ìë¬ ë°ì
- ë¡ê·¸ì¸ì ëì§ë§ ì¸ì¦ë API í¸ì¶ ì¤í¨
ìì¸: Access Tokenê³¼ Refresh Tokenì SECRET í¤ê° ë¤ë¥¸ë°, ê°ì í¤ë¡ ê²ì¦íë ¤ê³ ìë
í´ê²°ë°©ë²:
- íê²½ë³ì íì¸:
ACCESS_TOKEN_SECRET,REFRESH_TOKEN_SECRET - ê° í í° íì ì ë§ë SECRET ì¬ì©
- íê²½ë³ìê° ì ëë¡ ë¡ëëëì§ íì¸ (
dotenvì´ê¸°í)
문ì 2: CORS ìë¬ë¡ íë¡ í¸ìëìì ë¡ê·¸ì¸ ë¶ê°
ì¦ì: ë¸ë¼ì°ì ì½ìì “CORS policy” ìë¬
ìì¸: Express ìë²ì CORS ì¤ì ëë½
í´ê²°ë°©ë²:
import cors from 'cors';
app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
credentials: true
}));
문ì 3: Refresh Tokenì´ ê³ì ë§ë£ë¨
ì¦ì: ì¬ì©ìê° ì주 ë¡ê·¸ììëë íì
ìì¸: Refresh Tokenì´ DBìì ì ëë¡ ê´ë¦¬ëì§ ìì
í´ê²°ë°©ë²:
- Refresh Token ìì± ì DBì ì ì¥ íì¸
- ë§ë£ ìê° ì ì í ì¤ì (ìµì 7ì¼)
- ë§ë£ë í í° ì 기ì ì¼ë¡ ì 리íë cron job ì¶ê°
References
ê³µì 문ì
ë¼ì´ë¸ë¬ë¦¬
- jsonwebtoken (Node.js)
- bcrypt (Node.js)
- Passport.js – ë¤ìí ì¸ì¦ ì ëµ
- NextAuth.js – Next.js ì¸ì¦
ë³´ì ê°ì´ë
Metadata
ë²ì
- íì¬ ë²ì : 1.0.0
- ìµì¢ ì ë°ì´í¸: 2025-01-01
- í¸í íë«í¼: Claude, ChatGPT, Gemini
ê´ë ¨ ì¤í¬
- api-design: API ìëí¬ì¸í¸ ì¤ê³
- security: ë³´ì ë² ì¤í¸ íëí°ì¤
íê·¸
#authentication #authorization #JWT #OAuth #security #backend