์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ” OAuth 2.0๊ณผ JWT ์ธ์ฆ ์™„์ „์ •๋ณต! ์‘์šฉํ”„๋กœ๊ทธ๋ž˜๋ฐ์—์„œ ์‹ค์ „ ์ ์šฉํ•˜๊ธฐ

๐Ÿ” OAuth 2.0๊ณผ JWT ์ธ์ฆ ์™„์ „์ •๋ณต! ์‘์šฉํ”„๋กœ๊ทธ๋ž˜๋ฐ์—์„œ ์‹ค์ „ ์ ์šฉํ•˜๊ธฐ

์•ˆ๋…•! ๐Ÿ‘‹ ์˜ค๋Š˜์€ ํ˜„๋Œ€ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋น ์งˆ ์ˆ˜ ์—†๋Š” ๋‘ ๊ฐ€์ง€ ํ•ต์‹ฌ ์ธ์ฆ ๊ธฐ์ˆ , OAuth 2.0๊ณผ JWT(JSON Web Token)์— ๋Œ€ํ•ด ๊นŠ์ด ์žˆ๊ฒŒ ํŒŒํ—ค์ณ๋ณผ ๊ฑฐ์•ผ. ํ˜น์‹œ "๋กœ๊ทธ์ธ ๊ตฌํ˜„์ด ์ด๋ ‡๊ฒŒ ๋ณต์žกํ•  ์ค„์ด์•ผ..." ํ•˜๋ฉด์„œ ๋จธ๋ฆฌ๋ฅผ ์ฅ์–ด๋œฏ์€ ์  ์žˆ์–ด? ๐Ÿ˜… ๊ฑฑ์ • ๋งˆ! ์ด ๊ธ€์„ ๋‹ค ์ฝ๊ณ  ๋‚˜๋ฉด ๋„ˆ๋„ ์ธ์ฆ ์ „๋ฌธ๊ฐ€๊ฐ€ ๋  ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ.

๐Ÿ‘ค ์‚ฌ์šฉ์ž ๐Ÿ–ฅ๏ธ ์„œ๋ฒ„ OAuth 2.0 ์ธ์ฆ ํ”„๋กœํ† ์ฝœ JWT ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ ํ˜„๋Œ€ ์ธ์ฆ ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ ๊ตฌ์กฐ

๐ŸŽฏ ์™œ OAuth 2.0๊ณผ JWT๋ฅผ ์•Œ์•„์•ผ ํ• ๊นŒ?

์š”์ฆ˜ ์›น ์„œ๋น„์Šค๋ฅผ ๋ณด๋ฉด "๊ตฌ๊ธ€๋กœ ๋กœ๊ทธ์ธ", "์นด์นด์˜ค๋กœ ์‹œ์ž‘ํ•˜๊ธฐ" ๊ฐ™์€ ๋ฒ„ํŠผ๋“ค ๋งŽ์ด ๋ดค์ง€? ๐Ÿค” ์ด๊ฒŒ ๋ฐ”๋กœ OAuth 2.0์„ ํ™œ์šฉํ•œ ์†Œ์…œ ๋กœ๊ทธ์ธ์ด์•ผ. ๊ทธ๋ฆฌ๊ณ  ๋กœ๊ทธ์ธ ํ›„์— ์„œ๋ฒ„๊ฐ€ ๋„ˆ๋ฅผ ๊ธฐ์–ตํ•˜๋Š” ๋ฐฉ๋ฒ• ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ JWT๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฑฐ์ง€.

์ „ํ†ต์ ์ธ ์„ธ์…˜ ๊ธฐ๋ฐ˜ ์ธ์ฆ์€ ์„œ๋ฒ„์— ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ €์žฅํ•ด์•ผ ํ–ˆ์–ด. ์‚ฌ์šฉ์ž๊ฐ€ ๋งŽ์•„์ง€๋ฉด? ์„œ๋ฒ„ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ํ„ฐ์ ธ๋ฒ„๋ฆฌ๋Š” ๊ฑฐ์•ผ! ๐Ÿ’ฅ ํ•˜์ง€๋งŒ JWT๋Š” ํ† ํฐ ์ž์ฒด์— ์ •๋ณด๋ฅผ ๋‹ด์•„์„œ ์„œ๋ฒ„ ๋ถ€๋‹ด์„ ํ™• ์ค„์—ฌ์ค˜. OAuth 2.0์€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ง์ ‘ ์ฃผ๊ณ ๋ฐ›์ง€ ์•Š๊ณ ๋„ ์•ˆ์ „ํ•˜๊ฒŒ ์ธ์ฆํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๊ณ  ๋ง์ด์•ผ.

๐Ÿ“Œ ์ด ๊ธ€์—์„œ ๋ฐฐ์šธ ํ•ต์‹ฌ ๋‚ด์šฉ

โœ… OAuth 2.0์˜ ์ž‘๋™ ์›๋ฆฌ์™€ 4๊ฐ€์ง€ ์ธ์ฆ ๋ฐฉ์‹
โœ… JWT์˜ ๊ตฌ์กฐ์™€ ์ƒ์„ฑ/๊ฒ€์ฆ ๋ฐฉ๋ฒ•
โœ… ์‹ค์ œ ์‘์šฉํ”„๋กœ๊ทธ๋ž˜๋ฐ์—์„œ์˜ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•
โœ… ๋ณด์•ˆ ์ทจ์•ฝ์ ๊ณผ ๋Œ€์‘ ์ „๋žต
โœ… ์‹ค์ „ ์ฝ”๋“œ ์˜ˆ์ œ์™€ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

๐Ÿ”‘ OAuth 2.0 ์™„์ „ ํ•ด๋ถ€ํ•˜๊ธฐ

OAuth 2.0์ด ๋ญ๊ธธ๋ž˜?

OAuth 2.0์€ ์ธ๊ฐ€(Authorization) ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๊ฑด "์ธ์ฆ(Authentication)"์ด ์•„๋‹ˆ๋ผ "์ธ๊ฐ€"๋ผ๋Š” ์ ! ๐ŸŽญ ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, "์ด ์‚ฌ๋žŒ์ด ๋ˆ„๊ตฌ์ธ์ง€"๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ "์ด ์‚ฌ๋žŒ์ด ๋ฌด์—‡์„ ํ•  ์ˆ˜ ์žˆ๋Š”์ง€"๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ๊ฑฐ์•ผ.

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ. ๋„ˆ๊ฐ€ ์–ด๋–ค ์‚ฌ์ง„ ํŽธ์ง‘ ์•ฑ์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ์ด ์•ฑ์ด ๊ตฌ๊ธ€ ๋“œ๋ผ์ด๋ธŒ์— ์žˆ๋Š” ๋„ˆ์˜ ์‚ฌ์ง„์— ์ ‘๊ทผํ•˜๊ณ  ์‹ถ์–ด ํ•ด. ์ด๋•Œ OAuth 2.0์„ ์‚ฌ์šฉํ•˜๋ฉด ๊ตฌ๊ธ€ ๊ณ„์ • ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•ฑ์— ์•Œ๋ ค์ฃผ์ง€ ์•Š๊ณ ๋„, "์ด ์•ฑ์ด ๋‚ด ๋“œ๋ผ์ด๋ธŒ์˜ ์‚ฌ์ง„๋งŒ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ํ—ˆ๋ฝํ• ๊ฒŒ"๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฑฐ์ง€! ๐ŸŽจ

OAuth 2.0์˜ ํ•ต์‹ฌ ๋“ฑ์žฅ์ธ๋ฌผ๋“ค

์—ญํ•  ์„ค๋ช… ์˜ˆ์‹œ
Resource Owner ๋ฆฌ์†Œ์Šค์˜ ์ฃผ์ธ, ์ฆ‰ ์‚ฌ์šฉ์ž ๊ตฌ๊ธ€ ๊ณ„์ •์„ ๊ฐ€์ง„ ๋„ˆ
Client ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•˜๋ ค๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‚ฌ์ง„ ํŽธ์ง‘ ์•ฑ
Authorization Server ์ธ์ฆ์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜๋Š” ์„œ๋ฒ„ ๊ตฌ๊ธ€ OAuth ์„œ๋ฒ„
Resource Server ์‹ค์ œ ๋ฆฌ์†Œ์Šค๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์„œ๋ฒ„ ๊ตฌ๊ธ€ ๋“œ๋ผ์ด๋ธŒ API ์„œ๋ฒ„

OAuth 2.0์˜ 4๊ฐ€์ง€ ์ธ์ฆ ๋ฐฉ์‹ (Grant Types)

OAuth 2.0์€ ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์ธ์ฆ ๋ฐฉ์‹์„ ์ œ๊ณตํ•ด. ๋งˆ์น˜ ์ƒํ™ฉ์— ๋งž๋Š” ์˜ท์„ ์ž…๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ง์ด์•ผ! ๐Ÿ‘”๐Ÿ‘—

1๏ธโƒฃ Authorization Code Grant (๊ฐ€์žฅ ์•ˆ์ „ํ•˜๊ณ  ๋งŽ์ด ์“ฐ์ž„)

์–ธ์ œ ์‚ฌ์šฉ? ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ฃผ๋กœ ์‚ฌ์šฉํ•ด. ๊ฐ€์žฅ ๋ณด์•ˆ์ด ๊ฐ•๋ ฅํ•œ ๋ฐฉ์‹์ด์•ผ!

์ด ๋ฐฉ์‹์˜ ํ๋ฆ„์„ ๋‹จ๊ณ„๋ณ„๋กœ ์‚ดํŽด๋ณผ๊ฒŒ:

Step 1: ์‚ฌ์šฉ์ž๊ฐ€ ํด๋ผ์ด์–ธํŠธ ์•ฑ์—์„œ "๊ตฌ๊ธ€๋กœ ๋กœ๊ทธ์ธ" ๋ฒ„ํŠผ์„ ํด๋ฆญ ๐Ÿ–ฑ๏ธ

Step 2: ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์‚ฌ์šฉ์ž๋ฅผ Authorization Server๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
์ด๋•Œ URL์— ์—ฌ๋Ÿฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํฌํ•จ์‹œ์ผœ:

https://accounts.google.com/o/oauth2/v2/auth?
  client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/callback
  &response_type=code
  &scope=profile email
  &state=random_string_for_security

Step 3: ์‚ฌ์šฉ์ž๊ฐ€ ๊ตฌ๊ธ€์— ๋กœ๊ทธ์ธํ•˜๊ณ  ๊ถŒํ•œ ์Šน์ธ โœ…
"์ด ์•ฑ์ด ๋‹น์‹ ์˜ ํ”„๋กœํ•„๊ณผ ์ด๋ฉ”์ผ์— ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์„ ํ—ˆ์šฉํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" ๊ฐ™์€ ํ™”๋ฉด์ด ๋‚˜์™€.

Step 4: Authorization Server๊ฐ€ Authorization Code๋ฅผ ๋ฐœ๊ธ‰ํ•˜๊ณ  redirect_uri๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ

https://yourapp.com/callback?code=AUTHORIZATION_CODE&state=random_string_for_security

Step 5: ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ด ์ฝ”๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  Authorization Server์— Access Token ์š”์ฒญ
์ด๋•Œ๋Š” ์„œ๋ฒ„ ๊ฐ„ ํ†ต์‹ ์ด๋ผ ์‚ฌ์šฉ์ž ๋ธŒ๋ผ์šฐ์ €๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š์•„. ๋” ์•ˆ์ „ํ•˜์ง€! ๐Ÿ”’

POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded

code=AUTHORIZATION_CODE
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&redirect_uri=https://yourapp.com/callback
&grant_type=authorization_code

Step 6: Authorization Server๊ฐ€ Access Token (๊ทธ๋ฆฌ๊ณ  ๋ณดํ†ต Refresh Token๋„) ๋ฐœ๊ธ‰ ๐ŸŽซ

{
  "access_token": "ya29.a0AfH6SMBx...",
  "expires_in": 3600,
  "refresh_token": "1//0gOZp8qT...",
  "scope": "profile email",
  "token_type": "Bearer"
}

Step 7: ์ด์ œ ์ด Access Token์œผ๋กœ Resource Server(๊ตฌ๊ธ€ ๋“œ๋ผ์ด๋ธŒ ๋“ฑ)์— API ์š”์ฒญ! ๐Ÿš€

๐Ÿ’ก ์™œ ์ด๋ ‡๊ฒŒ ๋ณต์žกํ•˜๊ฒŒ?
Authorization Code๋ฅผ ๋จผ์ € ๋ฐ›๊ณ , ๊ทธ๊ฑธ๋กœ ๋‹ค์‹œ ํ† ํฐ์„ ๋ฐ›๋Š” ์ด์œ ๋Š” ๋ณด์•ˆ ๋•Œ๋ฌธ์ด์•ผ. ๋ธŒ๋ผ์šฐ์ €๋ฅผ ํ†ตํ•ด ์ง์ ‘ ํ† ํฐ์„ ์ „๋‹ฌํ•˜๋ฉด ํƒˆ์ทจ๋  ์œ„ํ—˜์ด ์žˆ๊ฑฐ๋“ . ์ฝ”๋“œ๋Š” ํ•œ ๋ฒˆ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ณ , ์‹ค์ œ ํ† ํฐ ๊ตํ™˜์€ ์„œ๋ฒ„ ๊ฐ„์— ์•ˆ์ „ํ•˜๊ฒŒ ์ด๋ฃจ์–ด์ ธ!

2๏ธโƒฃ Implicit Grant (๋‹จ์ˆœํ•˜์ง€๋งŒ ๋ณด์•ˆ ์•ฝํ•จ)

์˜ˆ์ „์—๋Š” SPA(Single Page Application)์—์„œ ๋งŽ์ด ์ผ๋Š”๋ฐ, ์š”์ฆ˜์€ ๋ณด์•ˆ ๋ฌธ์ œ๋กœ ๊ถŒ์žฅํ•˜์ง€ ์•Š์•„. ๐Ÿ˜ฐ Authorization Code ์—†์ด ๋ฐ”๋กœ Access Token์„ ๋ฐ›๋Š” ๋ฐฉ์‹์ด์•ผ.

https://accounts.google.com/o/oauth2/v2/auth?
  client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/callback
  &response_type=token
  &scope=profile email

๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์‹œ URL fragment(#)์— ํ† ํฐ์ด ๋ฐ”๋กœ ํฌํ•จ๋ผ:

https://yourapp.com/callback#access_token=ya29.a0AfH6SMBx...&token_type=Bearer&expires_in=3600

โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜!
์ด ๋ฐฉ์‹์€ ํ† ํฐ์ด ๋ธŒ๋ผ์šฐ์ € ํžˆ์Šคํ† ๋ฆฌ์— ๋‚จ์„ ์ˆ˜ ์žˆ๊ณ , Refresh Token๋„ ๋ฐœ๊ธ‰๋˜์ง€ ์•Š์•„. ํ˜„์žฌ๋Š” Authorization Code Grant + PKCE๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ๊ถŒ์žฅ๋ผ!

3๏ธโƒฃ Resource Owner Password Credentials Grant

์‚ฌ์šฉ์ž๊ฐ€ ํด๋ผ์ด์–ธํŠธ์— ์ง์ ‘ ์•„์ด๋””/๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜๋Š” ๋ฐฉ์‹. ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์ž์‚ฌ ์•ฑ์—์„œ๋งŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ด! ๐Ÿข

POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=password
&username=user@example.com
&password=user_password
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

์ด ๋ฐฉ์‹์€ OAuth์˜ ์ฒ ํ•™(๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๊ณต์œ ํ•˜์ง€ ์•Š๊ธฐ)์— ์–ด๊ธ‹๋‚˜์„œ ๋ ˆ๊ฑฐ์‹œ ์‹œ์Šคํ…œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ™์€ ํŠน์ˆ˜ํ•œ ๊ฒฝ์šฐ๊ฐ€ ์•„๋‹ˆ๋ฉด ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒŒ ์ข‹์•„.

4๏ธโƒฃ Client Credentials Grant

์‚ฌ์šฉ์ž๊ฐ€ ์•„๋‹Œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ž์ฒด๊ฐ€ ์ธ์ฆ๋ฐ›๋Š” ๋ฐฉ์‹. ์„œ๋ฒ„ ๊ฐ„ ํ†ต์‹ (Machine-to-Machine)์—์„œ ์‚ฌ์šฉํ•ด! ๐Ÿค–

POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&scope=api.read

์˜ˆ๋ฅผ ๋“ค์–ด, ๋„ˆ์˜ ๋ฐฑ์—”๋“œ ์„œ๋ฒ„๊ฐ€ ๋‹ค๋ฅธ ํšŒ์‚ฌ์˜ API๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ฑฐ์•ผ. ์‚ฌ์šฉ์ž ์ปจํ…์ŠคํŠธ๊ฐ€ ์—†๊ณ  ์ˆœ์ˆ˜ํ•˜๊ฒŒ ์•ฑ ๋Œ€ ์•ฑ ํ†ต์‹ ์ด์ง€.

PKCE: ๋ชจ๋ฐ”์ผ/SPA๋ฅผ ์œ„ํ•œ ๋ณด์•ˆ ๊ฐ•ํ™”

PKCE(Proof Key for Code Exchange, "ํ”ฝ์‹œ"๋ผ๊ณ  ์ฝ์–ด)๋Š” Authorization Code Grant๋ฅผ ๋” ์•ˆ์ „ํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๋Š” ํ™•์žฅ ๊ธฐ๋Šฅ์ด์•ผ. ๐Ÿ“ฑ ํŠนํžˆ ๋ชจ๋ฐ”์ผ ์•ฑ์ด๋‚˜ SPA์ฒ˜๋Ÿผ ํด๋ผ์ด์–ธํŠธ ์‹œํฌ๋ฆฟ์„ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด๊ด€ํ•  ์ˆ˜ ์—†๋Š” ํ™˜๊ฒฝ์—์„œ ํ•„์ˆ˜์•ผ!

PKCE ์ž‘๋™ ์›๋ฆฌ

1๋‹จ๊ณ„: ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋žœ๋คํ•œ code_verifier ์ƒ์„ฑ (43~128์ž์˜ ๋žœ๋ค ๋ฌธ์ž์—ด)

2๋‹จ๊ณ„: code_verifier๋ฅผ SHA256์œผ๋กœ ํ•ด์‹ฑํ•˜์—ฌ code_challenge ์ƒ์„ฑ

3๋‹จ๊ณ„: Authorization ์š”์ฒญ ์‹œ code_challenge๋ฅผ ํ•จ๊ป˜ ์ „์†ก

4๋‹จ๊ณ„: Token ์š”์ฒญ ์‹œ ์›๋ณธ code_verifier๋ฅผ ํ•จ๊ป˜ ์ „์†ก

5๋‹จ๊ณ„: ์„œ๋ฒ„๊ฐ€ code_verifier๋ฅผ ํ•ด์‹ฑํ•ด์„œ ์ฒ˜์Œ ๋ฐ›์€ code_challenge์™€ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธ โœ“

// JavaScript ์˜ˆ์ œ
// 1. Code Verifier ์ƒ์„ฑ
function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64URLEncode(array);
}

// 2. Code Challenge ์ƒ์„ฑ
async function generateCodeChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return base64URLEncode(new Uint8Array(hash));
}

// 3. Authorization ์š”์ฒญ
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);

window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?
  client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/callback
  &response_type=code
  &scope=profile email
  &code_challenge=${codeChallenge}
  &code_challenge_method=S256`;

// 4. Token ์š”์ฒญ ์‹œ code_verifier ํฌํ•จ
fetch('https://oauth2.googleapis.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    code: authorizationCode,
    client_id: 'YOUR_CLIENT_ID',
    redirect_uri: 'https://yourapp.com/callback',
    grant_type: 'authorization_code',
    code_verifier: codeVerifier
  })
});

๐ŸŽซ JWT(JSON Web Token) ๋งˆ์Šคํ„ฐํ•˜๊ธฐ

JWT๊ฐ€ ๋ญ์•ผ?

JWT๋Š” JSON ํ˜•์‹์˜ ์ •๋ณด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ „์†กํ•˜๊ธฐ ์œ„ํ•œ ํ† ํฐ์ด์•ผ. ์„ธ ๋ถ€๋ถ„์œผ๋กœ ๋‚˜๋‰˜์–ด์ ธ ์žˆ๊ณ , ์ (.)์œผ๋กœ ๊ตฌ๋ถ„๋ผ ์žˆ์–ด:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

[Header].[Payload].[Signature]

์ด ์ด์ƒํ•œ ๋ฌธ์ž์—ด์ด ์‹ค์ œ๋กœ๋Š” ์—„์ฒญ๋‚œ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ์–ด! ๐ŸŽ ํ•˜๋‚˜์”ฉ ๋œฏ์–ด๋ณผ๊นŒ?

JWT์˜ 3๊ฐ€์ง€ ๊ตฌ์„ฑ ์š”์†Œ

1. Header (ํ—ค๋”)

ํ† ํฐ์˜ ํƒ€์ž…๊ณผ ํ•ด์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ์–ด:

{
  "alg": "HS256",  // ์•Œ๊ณ ๋ฆฌ์ฆ˜: HMAC SHA256
  "typ": "JWT"     // ํƒ€์ž…: JWT
}

์ด๊ฑธ Base64Url๋กœ ์ธ์ฝ”๋”ฉํ•˜๋ฉด eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9๊ฐ€ ๋ผ.

2. Payload (ํŽ˜์ด๋กœ๋“œ)

์‹ค์ œ ์ „๋‹ฌํ•˜๋ ค๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด๊ฐ€๋Š” ๋ถ€๋ถ„! ์—ฌ๊ธฐ์—๋Š” Claim์ด๋ผ๋Š” ์ •๋ณด ์กฐ๊ฐ๋“ค์ด ๋“ค์–ด๊ฐ€:

{
  "sub": "1234567890",        // Subject: ํ† ํฐ ์ฃผ์ฒด (์‚ฌ์šฉ์ž ID)
  "name": "John Doe",         // ์‚ฌ์šฉ์ž ์ด๋ฆ„
  "email": "john@example.com",
  "role": "admin",            // ์‚ฌ์šฉ์ž ์—ญํ• 
  "iat": 1516239022,          // Issued At: ๋ฐœ๊ธ‰ ์‹œ๊ฐ„
  "exp": 1516242622           // Expiration: ๋งŒ๋ฃŒ ์‹œ๊ฐ„
}

๐Ÿ“‹ Claim์˜ 3๊ฐ€์ง€ ์ข…๋ฅ˜

Registered Claims (๋“ฑ๋ก๋œ ํด๋ ˆ์ž„): JWT ํ‘œ์ค€์—์„œ ์ •์˜ํ•œ ๊ฒƒ๋“ค
- iss (issuer): ๋ฐœ๊ธ‰์ž
- sub (subject): ์ฃผ์ œ
- aud (audience): ๋Œ€์ƒ
- exp (expiration): ๋งŒ๋ฃŒ ์‹œ๊ฐ„
- iat (issued at): ๋ฐœ๊ธ‰ ์‹œ๊ฐ„
- jti (JWT ID): ๊ณ ์œ  ์‹๋ณ„์ž

Public Claims: ์ถฉ๋Œ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด URI ํ˜•์‹์œผ๋กœ ์ •์˜
Private Claims: ๋‹น์‚ฌ์ž ๊ฐ„ ํ•ฉ์˜ํ•˜์— ์‚ฌ์šฉํ•˜๋Š” ์ปค์Šคํ…€ ํด๋ ˆ์ž„

โš ๏ธ ์ค‘์š”ํ•œ ๋ณด์•ˆ ํŒ!
Payload๋Š” Base64๋กœ ์ธ์ฝ”๋”ฉ๋งŒ ๋˜์–ด ์žˆ์–ด์„œ ๋ˆ„๊ตฌ๋‚˜ ๋””์ฝ”๋”ฉํ•ด์„œ ๋ณผ ์ˆ˜ ์žˆ์–ด! ๋”ฐ๋ผ์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋‚˜ ๋ฏผ๊ฐํ•œ ๊ฐœ์ธ์ •๋ณด๋Š” ์ ˆ๋Œ€ ๋„ฃ์œผ๋ฉด ์•ˆ ๋ผ! ๐Ÿšซ

3. Signature (์„œ๋ช…)

ํ† ํฐ์ด ๋ณ€์กฐ๋˜์ง€ ์•Š์•˜์Œ์„ ์ฆ๋ช…ํ•˜๋Š” ๋ถ€๋ถ„์ด์•ผ. Header์™€ Payload๋ฅผ ํ•ฉ์น˜๊ณ , ๋น„๋ฐ€ ํ‚ค๋กœ ์„œ๋ช…ํ•ด:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

์ด ์„œ๋ช… ๋•๋ถ„์— ๋ˆ„๊ตฐ๊ฐ€ ํ† ํฐ ๋‚ด์šฉ์„ ์กฐ์ž‘ํ•˜๋ฉด ๋ฐ”๋กœ ์•Œ ์ˆ˜ ์žˆ์–ด! ๐Ÿ” ์„œ๋ฒ„๋งŒ ์•Œ๊ณ  ์žˆ๋Š” ๋น„๋ฐ€ ํ‚ค๋กœ ์„œ๋ช…ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—, ๊ฐ™์€ ํ‚ค๋กœ ๊ฒ€์ฆํ•˜๋ฉด ์œ„์กฐ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์ง€.

JWT ์‹ค์ „ ๊ตฌํ˜„ํ•˜๊ธฐ

Node.js์—์„œ JWT ์ƒ์„ฑํ•˜๊ธฐ

// jsonwebtoken ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ
const jwt = require('jsonwebtoken');

// JWT ์ƒ์„ฑ
function generateToken(user) {
  const payload = {
    sub: user.id,
    email: user.email,
    role: user.role,
    iat: Math.floor(Date.now() / 1000)
  };
  
  const secret = process.env.JWT_SECRET; // ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ ๋น„๋ฐ€ํ‚ค ๊ฐ€์ ธ์˜ค๊ธฐ
  const options = {
    expiresIn: '1h',  // 1์‹œ๊ฐ„ ํ›„ ๋งŒ๋ฃŒ
    issuer: 'myapp.com'
  };
  
  return jwt.sign(payload, secret, options);
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
const user = { id: 123, email: 'user@example.com', role: 'user' };
const token = generateToken(user);
console.log(token);

JWT ๊ฒ€์ฆํ•˜๊ธฐ

// JWT ๊ฒ€์ฆ ๋ฏธ๋“ค์›จ์–ด
function authenticateToken(req, res, next) {
  // Authorization ํ—ค๋”์—์„œ ํ† ํฐ ์ถ”์ถœ
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"
  
  if (!token) {
    return res.status(401).json({ error: 'ํ† ํฐ์ด ์—†์Šต๋‹ˆ๋‹ค' });
  }
  
  jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
    if (err) {
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ error: 'ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค' });
      }
      return res.status(403).json({ error: '์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค' });
    }
    
    // ๊ฒ€์ฆ ์„ฑ๊ณต! ๋””์ฝ”๋”ฉ๋œ ์ •๋ณด๋ฅผ req์— ์ €์žฅ
    req.user = decoded;
    next();
  });
}

// ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ์— ์ ์šฉ
app.get('/api/profile', authenticateToken, (req, res) => {
  // req.user์— ํ† ํฐ์˜ payload ์ •๋ณด๊ฐ€ ๋“ค์–ด์žˆ์Œ
  res.json({
    message: 'ํ”„๋กœํ•„ ์ •๋ณด',
    user: req.user
  });
});

Python (Flask)์—์„œ JWT ์‚ฌ์šฉํ•˜๊ธฐ

from flask import Flask, request, jsonify
from flask_jwt_extended import (
    JWTManager, create_access_token, 
    jwt_required, get_jwt_identity
)
from datetime import timedelta

app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'your-secret-key'
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1)
jwt = JWTManager(app)

# ๋กœ๊ทธ์ธ ์—”๋“œํฌ์ธํŠธ
@app.route('/login', methods=['POST'])
def login():
    username = request.json.get('username')
    password = request.json.get('password')
    
    # ์‹ค์ œ๋กœ๋Š” DB์—์„œ ์‚ฌ์šฉ์ž ํ™•์ธ
    if username == 'test' and password == 'password':
        # JWT ์ƒ์„ฑ
        access_token = create_access_token(
            identity=username,
            additional_claims={'role': 'admin'}
        )
        return jsonify(access_token=access_token), 200
    
    return jsonify(error='Invalid credentials'), 401

# ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ
@app.route('/protected', methods=['GET'])
@jwt_required()
def protected():
    current_user = get_jwt_identity()
    return jsonify(logged_in_as=current_user), 200

Java (Spring Boot)์—์„œ JWT ๊ตฌํ˜„

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;

public class JwtUtil {
    private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    private static final long EXPIRATION_TIME = 3600000; // 1์‹œ๊ฐ„
    
    // JWT ์ƒ์„ฑ
    public static String generateToken(String userId, String role) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + EXPIRATION_TIME);
        
        return Jwts.builder()
                .setSubject(userId)
                .claim("role", role)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(SECRET_KEY)
                .compact();
    }
    
    // JWT ๊ฒ€์ฆ ๋ฐ ํŒŒ์‹ฑ
    public static Claims validateToken(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(SECRET_KEY)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            throw new RuntimeException("ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค");
        } catch (JwtException e) {
            throw new RuntimeException("์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค");
        }
    }
    
    // ์‚ฌ์šฉ์ž ID ์ถ”์ถœ
    public static String getUserIdFromToken(String token) {
        Claims claims = validateToken(token);
        return claims.getSubject();
    }
}

Access Token vs Refresh Token ์ „๋žต

์‹ค์ „์—์„œ๋Š” ๋ณดํ†ต ๋‘ ๊ฐ€์ง€ ํ† ํฐ์„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด! ๐ŸŽญ

๐ŸŽซ Access Token (์ ‘๊ทผ ํ† ํฐ)

๋ชฉ์ : ์‹ค์ œ API ์š”์ฒญ์— ์‚ฌ์šฉ
์œ ํšจ๊ธฐ๊ฐ„: ์งง๊ฒŒ (15๋ถ„ ~ 1์‹œ๊ฐ„)
์ €์žฅ ์œ„์น˜: ๋ฉ”๋ชจ๋ฆฌ ๋˜๋Š” ์งง์€ ์ˆ˜๋ช…์˜ ์ฟ ํ‚ค
ํŠน์ง•: ํƒˆ์ทจ๋˜์–ด๋„ ํ”ผํ•ด๋ฅผ ์ตœ์†Œํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์งง์€ ์ˆ˜๋ช…

๐Ÿ”„ Refresh Token (๊ฐฑ์‹  ํ† ํฐ)

๋ชฉ์ : Access Token์ด ๋งŒ๋ฃŒ๋˜๋ฉด ์ƒˆ๋กœ ๋ฐœ๊ธ‰๋ฐ›๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
์œ ํšจ๊ธฐ๊ฐ„: ๊ธธ๊ฒŒ (1์ฃผ์ผ ~ 1๊ฐœ์›”)
์ €์žฅ ์œ„์น˜: HttpOnly ์ฟ ํ‚ค ๋˜๋Š” ์•ˆ์ „ํ•œ ์ €์žฅ์†Œ
ํŠน์ง•: ํ•œ ๋ฒˆ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ตฌํ˜„ (Rotation ์ „๋žต)

ํ† ํฐ ๊ฐฑ์‹  ํ”Œ๋กœ์šฐ ๊ตฌํ˜„

// Node.js ์˜ˆ์ œ
const express = require('express');
const jwt = require('jsonwebtoken');
const router = express.Router();

// Refresh Token ์ €์žฅ์†Œ (์‹ค์ œ๋กœ๋Š” Redis๋‚˜ DB ์‚ฌ์šฉ)
const refreshTokens = new Set();

// ๋กœ๊ทธ์ธ
router.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // ์‚ฌ์šฉ์ž ์ธ์ฆ (์‹ค์ œ๋กœ๋Š” DB ํ™•์ธ)
  const user = await authenticateUser(username, password);
  if (!user) {
    return res.status(401).json({ error: '์ธ์ฆ ์‹คํŒจ' });
  }
  
  // Access Token ์ƒ์„ฑ (15๋ถ„)
  const accessToken = jwt.sign(
    { sub: user.id, email: user.email },
    process.env.ACCESS_TOKEN_SECRET,
    { expiresIn: '15m' }
  );
  
  // Refresh Token ์ƒ์„ฑ (7์ผ)
  const refreshToken = jwt.sign(
    { sub: user.id },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: '7d' }
  );
  
  // Refresh Token ์ €์žฅ
  refreshTokens.add(refreshToken);
  
  // Refresh Token์€ HttpOnly ์ฟ ํ‚ค๋กœ ์ „์†ก
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7์ผ
  });
  
  res.json({ accessToken });
});

// Access Token ๊ฐฑ์‹ 
router.post('/refresh', (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  
  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh Token์ด ์—†์Šต๋‹ˆ๋‹ค' });
  }
  
  if (!refreshTokens.has(refreshToken)) {
    return res.status(403).json({ error: '์œ ํšจํ•˜์ง€ ์•Š์€ Refresh Token' });
  }
  
  jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, decoded) => {
    if (err) {
      return res.status(403).json({ error: 'Refresh Token ๊ฒ€์ฆ ์‹คํŒจ' });
    }
    
    // ์ƒˆ๋กœ์šด Access Token ๋ฐœ๊ธ‰
    const newAccessToken = jwt.sign(
      { sub: decoded.sub },
      process.env.ACCESS_TOKEN_SECRET,
      { expiresIn: '15m' }
    );
    
    // Refresh Token Rotation (์„ ํƒ์‚ฌํ•ญ์ด์ง€๋งŒ ๊ถŒ์žฅ)
    refreshTokens.delete(refreshToken);
    const newRefreshToken = jwt.sign(
      { sub: decoded.sub },
      process.env.REFRESH_TOKEN_SECRET,
      { expiresIn: '7d' }
    );
    refreshTokens.add(newRefreshToken);
    
    res.cookie('refreshToken', newRefreshToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000
    });
    
    res.json({ accessToken: newAccessToken });
  });
});

// ๋กœ๊ทธ์•„์›ƒ
router.post('/logout', (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  refreshTokens.delete(refreshToken);
  res.clearCookie('refreshToken');
  res.json({ message: '๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต' });
});

๐Ÿ”— OAuth 2.0 + JWT ํ†ตํ•ฉ ๊ตฌํ˜„ํ•˜๊ธฐ

์ด์ œ ์ง„์งœ ์‹ค์ „์ด์•ผ! OAuth 2.0์œผ๋กœ ์†Œ์…œ ๋กœ๊ทธ์ธ์„ ๊ตฌํ˜„ํ•˜๊ณ , JWT๋กœ ์„ธ์…˜์„ ๊ด€๋ฆฌํ•˜๋Š” ์™„์ „ํ•œ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ. ๐Ÿš€

์ „์ฒด ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„

OAuth 2.0 + JWT ํ†ตํ•ฉ ์ธ์ฆ ํ”Œ๋กœ์šฐ 1. ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์š”์ฒญ 2. ์šฐ๋ฆฌ ์„œ๋ฒ„ OAuth ํด๋ผ์ด์–ธํŠธ JWT ๋ฐœ๊ธ‰์ž 3. OAuth ์„œ๋ฒ„ (๊ตฌ๊ธ€/์นด์นด์˜ค ๋“ฑ) ์ธ์ฆ ์ฒ˜๋ฆฌ 4. API ์„œ๋ฒ„ JWT ๊ฒ€์ฆ ๊ตฌ๊ธ€๋กœ ๋กœ๊ทธ์ธ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ Access Token JWT ์ƒ์„ฑ Access + Refresh JWT ์ „๋‹ฌ ์ดํ›„ API ์š”์ฒญ JWT ํฌํ•จ Authorization: Bearer JWT ํ•ต์‹ฌ: OAuth๋กœ ์‹ ์› ํ™•์ธ โ†’ ์šฐ๋ฆฌ ์„œ๋ฒ„๊ฐ€ JWT ๋ฐœ๊ธ‰ โ†’ JWT๋กœ API ์ธ์ฆ

์‹ค์ „ ๊ตฌํ˜„: Express.js + Passport.js

1๋‹จ๊ณ„: ํ”„๋กœ์ ํŠธ ์„ค์ •

// package.json ์˜์กด์„ฑ
{
  "dependencies": {
    "express": "^4.18.2",
    "passport": "^0.6.0",
    "passport-google-oauth20": "^2.0.0",
    "jsonwebtoken": "^9.0.0",
    "cookie-parser": "^1.4.6",
    "dotenv": "^16.0.3"
  }
}
// .env ํŒŒ์ผ
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback
JWT_ACCESS_SECRET=your_access_token_secret
JWT_REFRESH_SECRET=your_refresh_token_secret
SESSION_SECRET=your_session_secret

2๋‹จ๊ณ„: Passport ์„ค์ •

// config/passport.js
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const User = require('../models/User'); // ์‚ฌ์šฉ์ž ๋ชจ๋ธ

passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: process.env.GOOGLE_CALLBACK_URL
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      // ๊ธฐ์กด ์‚ฌ์šฉ์ž ์ฐพ๊ธฐ
      let user = await User.findOne({ googleId: profile.id });
      
      if (!user) {
        // ์ƒˆ ์‚ฌ์šฉ์ž ์ƒ์„ฑ
        user = await User.create({
          googleId: profile.id,
          email: profile.emails[0].value,
          name: profile.displayName,
          avatar: profile.photos[0].value
        });
      }
      
      return done(null, user);
    } catch (error) {
      return done(error, null);
    }
  }
));

module.exports = passport;

3๋‹จ๊ณ„: JWT ์œ ํ‹ธ๋ฆฌํ‹ฐ

// utils/jwt.js
const jwt = require('jsonwebtoken');

class JWTService {
  // Access Token ์ƒ์„ฑ (15๋ถ„)
  static generateAccessToken(user) {
    return jwt.sign(
      {
        sub: user.id,
        email: user.email,
        role: user.role || 'user'
      },
      process.env.JWT_ACCESS_SECRET,
      { expiresIn: '15m' }
    );
  }
  
  // Refresh Token ์ƒ์„ฑ (7์ผ)
  static generateRefreshToken(user) {
    return jwt.sign(
      { sub: user.id },
      process.env.JWT_REFRESH_SECRET,
      { expiresIn: '7d' }
    );
  }
  
  // Access Token ๊ฒ€์ฆ
  static verifyAccessToken(token) {
    try {
      return jwt.verify(token, process.env.JWT_ACCESS_SECRET);
    } catch (error) {
      throw new Error('Invalid access token');
    }
  }
  
  // Refresh Token ๊ฒ€์ฆ
  static verifyRefreshToken(token) {
    try {
      return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
    } catch (error) {
      throw new Error('Invalid refresh token');
    }
  }
}

module.exports = JWTService;

4๋‹จ๊ณ„: ์ธ์ฆ ๋ผ์šฐํŠธ

// routes/auth.js
const express = require('express');
const passport = require('passport');
const JWTService = require('../utils/jwt');
const router = express.Router();

// ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ์‹œ์ž‘
router.get('/google',
  passport.authenticate('google', {
    scope: ['profile', 'email'],
    session: false // ์„ธ์…˜ ์‚ฌ์šฉ ์•ˆ ํ•จ (JWT ์‚ฌ์šฉ)
  })
);

// ๊ตฌ๊ธ€ ์ฝœ๋ฐฑ
router.get('/google/callback',
  passport.authenticate('google', { 
    session: false,
    failureRedirect: '/login'
  }),
  (req, res) => {
    // OAuth ์ธ์ฆ ์„ฑ๊ณต! ์ด์ œ JWT ๋ฐœ๊ธ‰
    const accessToken = JWTService.generateAccessToken(req.user);
    const refreshToken = JWTService.generateRefreshToken(req.user);
    
    // Refresh Token์„ HttpOnly ์ฟ ํ‚ค๋กœ ์ €์žฅ
    res.cookie('refreshToken', refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000 // 7์ผ
    });
    
    // Access Token์€ ํด๋ผ์ด์–ธํŠธ๋กœ ์ „๋‹ฌ (์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ• ๊ฐ€๋Šฅ)
    // ๋ฐฉ๋ฒ• 1: ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ”„๋ก ํŠธ์—”๋“œ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
    res.redirect(`http://localhost:3000/auth-success?token=${accessToken}`);
    
    // ๋ฐฉ๋ฒ• 2: JSON ์‘๋‹ต (SPA์˜ ๊ฒฝ์šฐ)
    // res.json({ accessToken, user: req.user });
  }
);

// Token ๊ฐฑ์‹ 
router.post('/refresh', async (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  
  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh token not found' });
  }
  
  try {
    const decoded = JWTService.verifyRefreshToken(refreshToken);
    const user = await User.findById(decoded.sub);
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    
    // ์ƒˆ Access Token ๋ฐœ๊ธ‰
    const newAccessToken = JWTService.generateAccessToken(user);
    
    // Refresh Token Rotation (์„ ํƒ์‚ฌํ•ญ)
    const newRefreshToken = JWTService.generateRefreshToken(user);
    res.cookie('refreshToken', newRefreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000
    });
    
    res.json({ accessToken: newAccessToken });
  } catch (error) {
    res.status(403).json({ error: 'Invalid refresh token' });
  }
});

// ๋กœ๊ทธ์•„์›ƒ
router.post('/logout', (req, res) => {
  res.clearCookie('refreshToken');
  res.json({ message: 'Logged out successfully' });
});

module.exports = router;

5๋‹จ๊ณ„: JWT ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด

// middleware/auth.js
const JWTService = require('../utils/jwt');
const User = require('../models/User');

async function authenticateJWT(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  const token = authHeader.substring(7); // "Bearer " ์ œ๊ฑฐ
  
  try {
    const decoded = JWTService.verifyAccessToken(token);
    
    // ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ (์„ ํƒ์‚ฌํ•ญ, ์บ์‹ฑ ๊ถŒ์žฅ)
    const user = await User.findById(decoded.sub);
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    
    req.user = user;
    req.userId = decoded.sub;
    next();
  } catch (error) {
    if (error.message === 'jwt expired') {
      return res.status(401).json({ 
        error: 'Token expired',
        code: 'TOKEN_EXPIRED'
      });
    }
    return res.status(403).json({ error: 'Invalid token' });
  }
}

// ์—ญํ•  ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด
function authorize(...roles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Unauthorized' });
    }
    
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    
    next();
  };
}

module.exports = { authenticateJWT, authorize };

6๋‹จ๊ณ„: ๋ณดํ˜ธ๋œ API ๋ผ์šฐํŠธ

// routes/api.js
const express = require('express');
const { authenticateJWT, authorize } = require('../middleware/auth');
const router = express.Router();

// ์ธ์ฆ ํ•„์š”ํ•œ ๋ผ์šฐํŠธ
router.get('/profile', authenticateJWT, (req, res) => {
  res.json({
    user: {
      id: req.user.id,
      email: req.user.email,
      name: req.user.name,
      avatar: req.user.avatar
    }
  });
});

// ๊ด€๋ฆฌ์ž๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ
router.get('/admin/users', 
  authenticateJWT, 
  authorize('admin'), 
  async (req, res) => {
    const users = await User.find().select('-password');
    res.json({ users });
  }
);

// ์‚ฌ์šฉ์ž ์ •๋ณด ์ˆ˜์ •
router.put('/profile', authenticateJWT, async (req, res) => {
  const { name, bio } = req.body;
  
  try {
    const updatedUser = await User.findByIdAndUpdate(
      req.userId,
      { name, bio },
      { new: true }
    );
    
    res.json({ user: updatedUser });
  } catch (error) {
    res.status(500).json({ error: 'Update failed' });
  }
});

module.exports = router;

7๋‹จ๊ณ„: ๋ฉ”์ธ ์•ฑ ์„ค์ •

// app.js
const express = require('express');
const cookieParser = require('cookie-parser');
const passport = require('./config/passport');
require('dotenv').config();

const authRoutes = require('./routes/auth');
const apiRoutes = require('./routes/api');

const app = express();

// ๋ฏธ๋“ค์›จ์–ด
app.use(express.json());
app.use(cookieParser());
app.use(passport.initialize());

// CORS ์„ค์ • (ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์ธ ๊ฒฝ์šฐ)
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.header('Access-Control-Allow-Credentials', 'true');
  res.header('Access-Control-Allow-Headers', 'Authorization, Content-Type');
  next();
});

// ๋ผ์šฐํŠธ
app.use('/auth', authRoutes);
app.use('/api', apiRoutes);

// ์—๋Ÿฌ ํ•ธ๋“ค๋ง
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong!' });
});

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

ํ”„๋ก ํŠธ์—”๋“œ ํ†ตํ•ฉ (React ์˜ˆ์ œ)

// src/services/auth.js
class AuthService {
  constructor() {
    this.accessToken = null;
  }
  
  // ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ์‹œ์ž‘
  loginWithGoogle() {
    window.location.href = 'http://localhost:5000/auth/google';
  }
  
  // ์ฝœ๋ฐฑ์—์„œ ํ† ํฐ ์ €์žฅ
  handleAuthCallback(token) {
    this.accessToken = token;
    localStorage.setItem('accessToken', token);
  }
  
  // API ์š”์ฒญ
  async fetchWithAuth(url, options = {}) {
    const token = this.accessToken || localStorage.getItem('accessToken');
    
    const response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${token}`
      },
      credentials: 'include' // ์ฟ ํ‚ค ํฌํ•จ (Refresh Token)
    });
    
    // ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ ์ž๋™ ๊ฐฑ์‹ 
    if (response.status === 401) {
      const data = await response.json();
      if (data.code === 'TOKEN_EXPIRED') {
        const refreshed = await this.refreshToken();
        if (refreshed) {
          // ์žฌ์‹œ๋„
          return this.fetchWithAuth(url, options);
        }
      }
    }
    
    return response;
  }
  
  // ํ† ํฐ ๊ฐฑ์‹ 
  async refreshToken() {
    try {
      const response = await fetch('http://localhost:5000/auth/refresh', {
        method: 'POST',
        credentials: 'include'
      });
      
      if (response.ok) {
        const { accessToken } = await response.json();
        this.accessToken = accessToken;
        localStorage.setItem('accessToken', accessToken);
        return true;
      }
      
      return false;
    } catch (error) {
      console.error('Token refresh failed:', error);
      return false;
    }
  }
  
  // ๋กœ๊ทธ์•„์›ƒ
  async logout() {
    await fetch('http://localhost:5000/auth/logout', {
      method: 'POST',
      credentials: 'include'
    });
    
    this.accessToken = null;
    localStorage.removeItem('accessToken');
  }
}

export default new AuthService();
// src/components/Login.jsx
import React from 'react';
import AuthService from '../services/auth';

function Login() {
  const handleGoogleLogin = () => {
    AuthService.loginWithGoogle();
  };
  
  return (
    <div className="login-container">
      <h1>๋กœ๊ทธ์ธ</h1>
      <button onClick={handleGoogleLogin}>
        ๐Ÿ” ๊ตฌ๊ธ€๋กœ ๋กœ๊ทธ์ธ
      </button>
    </div>
  );
}

export default Login;
// src/components/AuthCallback.jsx
import React, { useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import AuthService from '../services/auth';

function AuthCallback() {
  const [searchParams] = useSearchParams();
  const navigate = useNavigate();
  
  useEffect(() => {
    const token = searchParams.get('token');
    if (token) {
      AuthService.handleAuthCallback(token);
      navigate('/dashboard');
    } else {
      navigate('/login');
    }
  }, [searchParams, navigate]);
  
  return <div>๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ์ค‘...</div>;
}

export default AuthCallback;
// src/components/Profile.jsx
import React, { useEffect, useState } from 'react';
import AuthService from '../services/auth';

function Profile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    loadProfile();
  }, []);
  
  const loadProfile = async () => {
    try {
      const response = await AuthService.fetchWithAuth(
        'http://localhost:5000/api/profile'
      );
      
      if (response.ok) {
        const data = await response.json();
        setUser(data.user);
      }
    } catch (error) {
      console.error('Failed to load profile:', error);
    } finally {
      setLoading(false);
    }
  };
  
  if (loading) return <div>๋กœ๋”ฉ ์ค‘...</div>;
  if (!user) return <div>์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.</div>;
  
  return (
    <div className="profile">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

export default Profile;

๐Ÿ›ก๏ธ ๋ณด์•ˆ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

์ธ์ฆ ์‹œ์Šคํ…œ์€ ๋ณด์•ˆ์ด ์ƒ๋ช…์ด์•ผ! ๐Ÿ” ์‹ค์ˆ˜ ํ•˜๋‚˜๋กœ ์ „์ฒด ์‹œ์Šคํ…œ์ด ๋šซ๋ฆด ์ˆ˜ ์žˆ์œผ๋‹ˆ ๊ผญ ์ง€์ผœ์•ผ ํ•  ์›์น™๋“ค์„ ์•Œ์•„๋ณด์ž.

1. ๋น„๋ฐ€ ํ‚ค ๊ด€๋ฆฌ

โŒ ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง์•„์•ผ ํ•  ๊ฒƒ๋“ค

โ€ข ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜๊ธฐ
โ€ข Git์— ์ปค๋ฐ‹ํ•˜๊ธฐ
โ€ข ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ์— ๋…ธ์ถœํ•˜๊ธฐ
โ€ข ์•ฝํ•œ ํ‚ค ์‚ฌ์šฉํ•˜๊ธฐ (์ตœ์†Œ 256๋น„ํŠธ)

โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•

โ€ข ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์‚ฌ์šฉ (.env ํŒŒ์ผ)
โ€ข AWS Secrets Manager, HashiCorp Vault ๊ฐ™์€ ๋น„๋ฐ€ ๊ด€๋ฆฌ ์„œ๋น„์Šค
โ€ข ํ”„๋กœ๋•์…˜๊ณผ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ํ‚ค ๋ถ„๋ฆฌ
โ€ข ์ •๊ธฐ์ ์ธ ํ‚ค ๋กœํ…Œ์ด์…˜

// ๊ฐ•๋ ฅํ•œ ๋น„๋ฐ€ ํ‚ค ์ƒ์„ฑ (Node.js)
const crypto = require('crypto');

// 256๋น„ํŠธ ๋žœ๋ค ํ‚ค ์ƒ์„ฑ
const generateSecret = () => {
  return crypto.randomBytes(32).toString('hex');
};

console.log('JWT_ACCESS_SECRET=' + generateSecret());
console.log('JWT_REFRESH_SECRET=' + generateSecret());

2. HTTPS ํ•„์ˆ˜

ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ HTTPS๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด! ๐Ÿ”’ HTTP๋กœ ํ† ํฐ์„ ์ „์†กํ•˜๋ฉด ์ค‘๊ฐ„์ž ๊ณต๊ฒฉ(Man-in-the-Middle)์— ์ทจ์•ฝํ•ด์ ธ.

// Express์—์„œ HTTPS ๊ฐ•์ œ
app.use((req, res, next) => {
  if (process.env.NODE_ENV === 'production' && !req.secure) {
    return res.redirect('https://' + req.headers.host + req.url);
  }
  next();
});

// ์ฟ ํ‚ค ์„ค์ • ์‹œ secure ํ”Œ๋ž˜๊ทธ
res.cookie('refreshToken', token, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production', // HTTPS์—์„œ๋งŒ
  sameSite: 'strict'
});

3. XSS (Cross-Site Scripting) ๋ฐฉ์–ด

XSS ๊ณต๊ฒฉ์€ ์•…์˜์ ์ธ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ฃผ์ž…ํ•ด์„œ ํ† ํฐ์„ ํ›”์ณ๊ฐ€๋Š” ๊ณต๊ฒฉ์ด์•ผ. ๐Ÿ˜ˆ

๋ฐฉ์–ด ์ „๋žต

1. Access Token์„ localStorage์— ์ €์žฅํ•˜์ง€ ์•Š๊ธฐ
localStorage๋Š” JavaScript๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ด์„œ XSS์— ์ทจ์•ฝํ•ด. ๋Œ€์‹ :
โ€ข ๋ฉ”๋ชจ๋ฆฌ์—๋งŒ ์ €์žฅ (์ƒˆ๋กœ๊ณ ์นจ ์‹œ ์žฌ๋ฐœ๊ธ‰)
โ€ข HttpOnly ์ฟ ํ‚ค ์‚ฌ์šฉ (JavaScript ์ ‘๊ทผ ๋ถˆ๊ฐ€)

2. Content Security Policy (CSP) ์„ค์ •

// Helmet ๋ฏธ๋“ค์›จ์–ด๋กœ CSP ์„ค์ •
const helmet = require('helmet');

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'"], // ๊ฐ€๋Šฅํ•˜๋ฉด 'unsafe-inline' ์ œ๊ฑฐ
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "https:"],
    connectSrc: ["'self'", "https://api.yourapp.com"]
  }
}));
// ์‚ฌ์šฉ์ž ์ž…๋ ฅ sanitization
const validator = require('validator');

app.post('/api/comment', authenticateJWT, (req, res) => {
  let { content } = req.body;
  
  // HTML ํƒœ๊ทธ ์ œ๊ฑฐ
  content = validator.escape(content);
  
  // ๋˜๋Š” DOMPurify ์‚ฌ์šฉ (ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ)
  // import DOMPurify from 'dompurify';
  // const clean = DOMPurify.sanitize(dirty);
  
  // DB ์ €์žฅ...
});

4. CSRF (Cross-Site Request Forgery) ๋ฐฉ์–ด

CSRF๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์˜๋„ํ•˜์ง€ ์•Š์€ ์š”์ฒญ์„ ๋ณด๋‚ด๋„๋ก ์†์ด๋Š” ๊ณต๊ฒฉ์ด์•ผ. ๐ŸŽฃ

// CSRF ํ† ํฐ ์‚ฌ์šฉ
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

app.use(csrfProtection);

app.get('/form', (req, res) => {
  res.render('form', { csrfToken: req.csrfToken() });
});

app.post('/api/transfer', csrfProtection, (req, res) => {
  // CSRF ํ† ํฐ์ด ์œ ํšจํ•œ ๊ฒฝ์šฐ์—๋งŒ ์‹คํ–‰
  // ...
});

JWT๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” SameSite ์ฟ ํ‚ค ์†์„ฑ์œผ๋กœ๋„ ๋ฐฉ์–ดํ•  ์ˆ˜ ์žˆ์–ด:

res.cookie('refreshToken', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict' // ๋˜๋Š” 'lax'
});

5. Rate Limiting (์†๋„ ์ œํ•œ)

๋ฌด์ฐจ๋ณ„ ๋Œ€์ž… ๊ณต๊ฒฉ(Brute Force)์„ ๋ง‰๊ธฐ ์œ„ํ•ด ์š”์ฒญ ํšŸ์ˆ˜๋ฅผ ์ œํ•œํ•ด์•ผ ํ•ด! โฑ๏ธ

const rateLimit = require('express-rate-limit');

// ๋กœ๊ทธ์ธ ์—”๋“œํฌ์ธํŠธ ์ œํ•œ
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15๋ถ„
  max: 5, // ์ตœ๋Œ€ 5๋ฒˆ ์‹œ๋„
  message: '๋„ˆ๋ฌด ๋งŽ์€ ๋กœ๊ทธ์ธ ์‹œ๋„์ž…๋‹ˆ๋‹ค. 15๋ถ„ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
  standardHeaders: true,
  legacyHeaders: false
});

app.post('/auth/login', loginLimiter, (req, res) => {
  // ๋กœ๊ทธ์ธ ๋กœ์ง
});

// API ์ „์ฒด ์ œํ•œ
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100
});

app.use('/api/', apiLimiter);

6. JWT ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ๊ตฌํ˜„

JWT๋Š” stateless๋ผ์„œ ๋ฐœ๊ธ‰ ํ›„ ์ทจ์†Œ๊ฐ€ ์–ด๋ ค์›Œ. ํ•˜์ง€๋งŒ ๋กœ๊ทธ์•„์›ƒ์ด๋‚˜ ๋ณด์•ˆ ์‚ฌ๊ณ  ์‹œ ํŠน์ • ํ† ํฐ์„ ๋ฌดํšจํ™”ํ•ด์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ์ง€. ๐Ÿšซ

// Redis๋ฅผ ์‚ฌ์šฉํ•œ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ
const redis = require('redis');
const client = redis.createClient();

// ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ์ถ”๊ฐ€
async function blacklistToken(token, expiresIn) {
  const decoded = jwt.decode(token);
  const remainingTime = decoded.exp - Math.floor(Date.now() / 1000);
  
  // ๋งŒ๋ฃŒ ์‹œ๊ฐ„๊นŒ์ง€๋งŒ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ์— ๋ณด๊ด€
  await client.setEx(`blacklist:${token}`, remainingTime, 'true');
}

// ํ† ํฐ ๊ฒ€์ฆ ์‹œ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ํ™•์ธ
async function isTokenBlacklisted(token) {
  const result = await client.get(`blacklist:${token}`);
  return result !== null;
}

// ๋ฏธ๋“ค์›จ์–ด์— ์ ์šฉ
async function authenticateJWT(req, res, next) {
  const token = extractToken(req);
  
  if (!token) {
    return res.status(401).json({ error: 'No token' });
  }
  
  // ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ํ™•์ธ
  if (await isTokenBlacklisted(token)) {
    return res.status(401).json({ error: 'Token has been revoked' });
  }
  
  // JWT ๊ฒ€์ฆ
  try {
    const decoded = JWTService.verifyAccessToken(token);
    req.user = decoded;
    next();
  } catch (error) {
    res.status(403).json({ error: 'Invalid token' });
  }
}

// ๋กœ๊ทธ์•„์›ƒ ์‹œ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ์ถ”๊ฐ€
app.post('/auth/logout', authenticateJWT, async (req, res) => {
  const token = extractToken(req);
  await blacklistToken(token, 900); // 15๋ถ„
  res.clearCookie('refreshToken');
  res.json({ message: 'Logged out' });
});

7. ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ •

ํ† ํฐ ํƒ€์ž… ๊ถŒ์žฅ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์ด์œ 
Access Token 15๋ถ„ ~ 1์‹œ๊ฐ„ ํƒˆ์ทจ ์‹œ ํ”ผํ•ด ์ตœ์†Œํ™”
Refresh Token 1์ฃผ์ผ ~ 1๊ฐœ์›” ์‚ฌ์šฉ์ž ํŽธ์˜์„ฑ ๊ณ ๋ ค
Email ์ธ์ฆ ํ† ํฐ 1์‹œ๊ฐ„ ~ 24์‹œ๊ฐ„ ๋ณด์•ˆ๊ณผ ์‚ฌ์šฉ์„ฑ ๊ท ํ˜•
๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ํ† ํฐ 15๋ถ„ ~ 1์‹œ๊ฐ„ ๋†’์€ ๋ณด์•ˆ ํ•„์š”

8. ๋กœ๊น…๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง

์˜์‹ฌ์Šค๋Ÿฌ์šด ํ™œ๋™์„ ๊ฐ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋กœ๊น…์€ ํ•„์ˆ˜์•ผ! ๐Ÿ“Š

// Winston ๋กœ๊ฑฐ ์„ค์ •
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// ์ธ์ฆ ์‹คํŒจ ๋กœ๊น…
app.post('/auth/login', async (req, res) => {
  const { email, password } = req.body;
  
  const user = await authenticateUser(email, password);
  
  if (!user) {
    logger.warn('Failed login attempt', {
      email,
      ip: req.ip,
      userAgent: req.headers['user-agent'],
      timestamp: new Date()
    });
    
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  logger.info('Successful login', {
    userId: user.id,
    email: user.email,
    ip: req.ip
  });
  
  // ํ† ํฐ ๋ฐœ๊ธ‰...
});

// ์˜์‹ฌ์Šค๋Ÿฌ์šด ํ™œ๋™ ๊ฐ์ง€
async function detectSuspiciousActivity(userId) {
  // ์ตœ๊ทผ 1์‹œ๊ฐ„ ๋™์•ˆ 5๊ฐœ ์ด์ƒ์˜ ๋‹ค๋ฅธ IP์—์„œ ๋กœ๊ทธ์ธ?
  const recentLogins = await getRecentLogins(userId, 3600);
  const uniqueIPs = new Set(recentLogins.map(l => l.ip));
  
  if (uniqueIPs.size >= 5) {
    logger.error('Suspicious activity detected', {
      userId,
      uniqueIPs: Array.from(uniqueIPs)
    });
    
    // ์•Œ๋ฆผ ์ „์†ก, ๊ณ„์ • ์ž ๊ธˆ ๋“ฑ
    await notifyUser(userId, 'suspicious_activity');
  }
}

๐Ÿš€ ๊ณ ๊ธ‰ ํŒจํ„ด๊ณผ ์ตœ์ ํ™”

1. Token Refresh ์ž๋™ํ™” (Silent Refresh)

์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ์„ ์‚ฌ์šฉํ•˜๋Š” ์ค‘์— ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜๋ฉด ์งœ์ฆ๋‚˜์ž–์•„? ๐Ÿ˜ค ์ž๋™์œผ๋กœ ๊ฐฑ์‹ ํ•ด์ฃผ๋Š” ํŒจํ„ด์„ ๊ตฌํ˜„ํ•ด๋ณด์ž!

// React์—์„œ ์ž๋™ ๊ฐฑ์‹  ๊ตฌํ˜„
import { useEffect, useRef } from 'react';
import AuthService from './services/auth';

function useTokenRefresh() {
  const refreshTimerRef = useRef(null);
  
  useEffect(() => {
    // Access Token ๋งŒ๋ฃŒ 5๋ถ„ ์ „์— ๊ฐฑ์‹ 
    const scheduleRefresh = () => {
      const token = localStorage.getItem('accessToken');
      if (!token) return;
      
      const decoded = JSON.parse(atob(token.split('.')[1]));
      const expiresAt = decoded.exp * 1000; // ๋ฐ€๋ฆฌ์ดˆ๋กœ ๋ณ€ํ™˜
      const now = Date.now();
      const timeUntilRefresh = expiresAt - now - (5 * 60 * 1000); // 5๋ถ„ ์ „
      
      if (timeUntilRefresh > 0) {
        refreshTimerRef.current = setTimeout(async () => {
          await AuthService.refreshToken();
          scheduleRefresh(); // ๋‹ค์Œ ๊ฐฑ์‹  ์˜ˆ์•ฝ
        }, timeUntilRefresh);
      }
    };
    
    scheduleRefresh();
    
    return () => {
      if (refreshTimerRef.current) {
        clearTimeout(refreshTimerRef.current);
      }
    };
  }, []);
}

// App.js์—์„œ ์‚ฌ์šฉ
function App() {
  useTokenRefresh();
  
  return (
    // ...
  );
}

2. ๋‹ค์ค‘ ๋””๋ฐ”์ด์Šค ์„ธ์…˜ ๊ด€๋ฆฌ

์‚ฌ์šฉ์ž๊ฐ€ ์—ฌ๋Ÿฌ ๊ธฐ๊ธฐ์—์„œ ๋กœ๊ทธ์ธํ–ˆ์„ ๋•Œ, ๊ฐ ์„ธ์…˜์„ ๊ด€๋ฆฌํ•˜๊ณ  ํ•„์š”์‹œ ํŠน์ • ์„ธ์…˜์„ ์ข…๋ฃŒํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด! ๐Ÿ“ฑ๐Ÿ’ป

// ์„ธ์…˜ ๋ชจ๋ธ
const sessionSchema = new mongoose.Schema({
  userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  refreshToken: { type: String, required: true },
  deviceInfo: {
    userAgent: String,
    ip: String,
    deviceType: String // 'mobile', 'desktop', 'tablet'
  },
  createdAt: { type: Date, default: Date.now },
  lastUsedAt: { type: Date, default: Date.now },
  expiresAt: Date
});

// ๋กœ๊ทธ์ธ ์‹œ ์„ธ์…˜ ์ƒ์„ฑ
router.post('/login', async (req, res) => {
  const user = await authenticateUser(req.body.email, req.body.password);
  
  const accessToken = JWTService.generateAccessToken(user);
  const refreshToken = JWTService.generateRefreshToken(user);
  
  // ์„ธ์…˜ ์ €์žฅ
  await Session.create({
    userId: user.id,
    refreshToken,
    deviceInfo: {
      userAgent: req.headers['user-agent'],
      ip: req.ip,
      deviceType: detectDeviceType(req.headers['user-agent'])
    },
    expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
  });
  
  res.cookie('refreshToken', refreshToken, { /* ... */ });
  res.json({ accessToken });
});

// ํ™œ์„ฑ ์„ธ์…˜ ๋ชฉ๋ก ์กฐํšŒ
router.get('/sessions', authenticateJWT, async (req, res) => {
  const sessions = await Session.find({ 
    userId: req.userId,
    expiresAt: { $gt: new Date() }
  }).sort({ lastUsedAt: -1 });
  
  res.json({ sessions });
});

// ํŠน์ • ์„ธ์…˜ ์ข…๋ฃŒ
router.delete('/sessions/:sessionId', authenticateJWT, async (req, res) => {
  const session = await Session.findOne({
    _id: req.params.sessionId,
    userId: req.userId
  });
  
  if (!session) {
    return res.status(404).json({ error: 'Session not found' });
  }
  
  // ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€
  await blacklistToken(session.refreshToken, 604800); // 7์ผ
  await session.remove();
  
  res.json({ message: 'Session terminated' });
});

// ๋ชจ๋“  ๋‹ค๋ฅธ ์„ธ์…˜ ์ข…๋ฃŒ
router.post('/sessions/terminate-others', authenticateJWT, async (req, res) => {
  const currentRefreshToken = req.cookies.refreshToken;
  
  const sessions = await Session.find({
    userId: req.userId,
    refreshToken: { $ne: currentRefreshToken }
  });
  
  // ๋ชจ๋“  ๋‹ค๋ฅธ ์„ธ์…˜์˜ ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ์ถ”๊ฐ€
  for (const session of sessions) {
    await blacklistToken(session.refreshToken, 604800);
  }
  
  await Session.deleteMany({
    userId: req.userId,
    refreshToken: { $ne: currentRefreshToken }
  });
  
  res.json({ message: `${sessions.length} sessions terminated` });
});

3. ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ณ„์ • ์—ฐ๋™

์‚ฌ์šฉ์ž๊ฐ€ ์—ฌ๋Ÿฌ ์†Œ์…œ ๊ณ„์ •์„ ํ•˜๋‚˜์˜ ๊ณ„์ •์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด๋ณด์ž! ๐Ÿ”—

// ์‚ฌ์šฉ์ž ๋ชจ๋ธ ํ™•์žฅ
const userSchema = new mongoose.Schema({
  email: String,
  name: String,
  avatar: String,
  providers: [{
    provider: String, // 'google', 'facebook', 'github'
    providerId: String,
    email: String,
    connectedAt: Date
  }]
});

// ๊ณ„์ • ์—ฐ๋™ ๋ผ์šฐํŠธ
router.get('/connect/google', 
  authenticateJWT, // ์ด๋ฏธ ๋กœ๊ทธ์ธ๋œ ์ƒํƒœ
  passport.authenticate('google', {
    scope: ['profile', 'email'],
    session: false,
    state: req.user.id // ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ์ „๋‹ฌ
  })
);

router.get('/connect/google/callback',
  passport.authenticate('google', { session: false }),
  async (req, res) => {
    const userId = req.query.state; // ์›๋ž˜ ์‚ฌ์šฉ์ž ID
    const googleProfile = req.user;
    
    // ์ด๋ฏธ ๋‹ค๋ฅธ ๊ณ„์ •์— ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ
    const existingUser = await User.findOne({
      'providers.provider': 'google',
      'providers.providerId': googleProfile.id
    });
    
    if (existingUser && existingUser.id !== userId) {
      return res.redirect('/settings?error=already_connected');
    }
    
    // ํ˜„์žฌ ์‚ฌ์šฉ์ž์— ๊ตฌ๊ธ€ ๊ณ„์ • ์—ฐ๊ฒฐ
    await User.findByIdAndUpdate(userId, {
      $push: {
        providers: {
          provider: 'google',
          providerId: googleProfile.id,
          email: googleProfile.emails[0].value,
          connectedAt: new Date()
        }
      }
    });
    
    res.redirect('/settings?success=connected');
  }
);

// ๊ณ„์ • ์—ฐ๊ฒฐ ํ•ด์ œ
router.delete('/connect/:provider', authenticateJWT, async (req, res) => {
  const user = await User.findById(req.userId);
  
  // ์ตœ์†Œ 1๊ฐœ์˜ ๋กœ๊ทธ์ธ ๋ฐฉ๋ฒ•์€ ๋‚จ๊ฒจ์•ผ ํ•จ
  if (user.providers.length <= 1 && !user.password) {
    return res.status(400).json({ 
      error: '์ตœ์†Œ 1๊ฐœ์˜ ๋กœ๊ทธ์ธ ๋ฐฉ๋ฒ•์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค' 
    });
  }
  
  await User.findByIdAndUpdate(req.userId, {
    $pull: {
      providers: { provider: req.params.provider }
    }
  });
  
  res.json({ message: 'Provider disconnected' });
});

4. JWT ํŽ˜์ด๋กœ๋“œ ์ตœ์ ํ™”

JWT๋Š” ๋ชจ๋“  ์š”์ฒญ์— ํฌํ•จ๋˜๋‹ˆ๊นŒ ํฌ๊ธฐ๋ฅผ ์ตœ์†Œํ™”ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด! ๐Ÿ“ฆ

์ตœ์ ํ™” ํŒ

โŒ ๋‚˜์œ ์˜ˆ:

{
  "userId": "507f1f77bcf86cd799439011",
  "userEmail": "user@example.com",
  "userName": "John Doe",
  "userRole": "admin",
  "userAvatar": "https://example.com/avatar.jpg",
  "userPreferences": { /* ... */ },
  "issuedAt": 1516239022,
  "expirationTime": 1516242622
}

โœ… ์ข‹์€ ์˜ˆ:

{
  "sub": "507f1f77bcf86cd799439011",
  "rol": "adm",
  "iat": 1516239022,
  "exp": 1516242622
}

ํ•„์š”ํ•œ ์ •๋ณด๋งŒ ๋‹ด๊ณ , ํ‚ค ์ด๋ฆ„์„ ์งง๊ฒŒ! ๋‚˜๋จธ์ง€ ์ •๋ณด๋Š” ์„œ๋ฒ„์—์„œ ์กฐํšŒํ•˜๊ฑฐ๋‚˜ ์บ์‹ฑํ•ด.

5. ์บ์‹ฑ ์ „๋žต

๋งค ์š”์ฒญ๋งˆ๋‹ค DB์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋ฉด ๋А๋ ค์ ธ. Redis๋กœ ์บ์‹ฑํ•˜์ž! โšก

const redis = require('redis');
const client = redis.createClient();

// ์‚ฌ์šฉ์ž ์ •๋ณด ์บ์‹ฑ
async function getUserFromCache(userId) {
  const cached = await client.get(`user:${userId}`);
  
  if (cached) {
    return JSON.parse(cached);
  }
  
  // ์บ์‹œ ๋ฏธ์Šค - DB์—์„œ ์กฐํšŒ
  const user = await User.findById(userId);
  
  if (user) {
    // 5๋ถ„๊ฐ„ ์บ์‹ฑ
    await client.setEx(
      `user:${userId}`,
      300,
      JSON.stringify(user)
    );
  }
  
  return user;
}

// ๋ฏธ๋“ค์›จ์–ด์— ์ ์šฉ
async function authenticateJWT(req, res, next) {
  const token = extractToken(req);
  
  try {
    const decoded = JWTService.verifyAccessToken(token);
    
    // ์บ์‹œ์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ
    const user = await getUserFromCache(decoded.sub);
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    
    req.user = user;
    next();
  } catch (error) {
    res.status(403).json({ error: 'Invalid token' });
  }
}

// ์‚ฌ์šฉ์ž ์ •๋ณด ์—…๋ฐ์ดํŠธ ์‹œ ์บ์‹œ ๋ฌดํšจํ™”
router.put('/profile', authenticateJWT, async (req, res) => {
  const updatedUser = await User.findByIdAndUpdate(
    req.userId,
    req.body,
    { new: true }
  );
  
  // ์บ์‹œ ์‚ญ์ œ
  await client.del(`user:${req.userId}`);
  
  res.json({ user: updatedUser });
});

๐Ÿ” ๋””๋ฒ„๊น…๊ณผ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋“ค

1. "Invalid Token" ์—๋Ÿฌ

์ฆ์ƒ: ํ† ํฐ์ด ์œ ํšจํ•ด ๋ณด์ด๋Š”๋ฐ ๊ณ„์† Invalid Token ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ

์›์ธ:
โ€ข ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ์˜ ๋น„๋ฐ€ ํ‚ค๊ฐ€ ๋‹ค๋ฆ„
โ€ข ํ† ํฐ์ด ์ž˜๋ชป ํŒŒ์‹ฑ๋จ (๊ณต๋ฐฑ, ์ค„๋ฐ”๊ฟˆ ํฌํ•จ)
โ€ข ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๋ถˆ์ผ์น˜ (HS256 vs RS256)

ํ•ด๊ฒฐ:

// ํ† ํฐ ๋””๋ฒ„๊น…
const token = req.headers.authorization?.split(' ')[1];
console.log('Raw token:', token);
console.log('Token length:', token?.length);

try {
  const decoded = jwt.decode(token, { complete: true });
  console.log('Decoded header:', decoded.header);
  console.log('Decoded payload:', decoded.payload);
  
  // ๊ฒ€์ฆ
  jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
  console.error('Verification error:', error.message);
}

2. CORS ์—๋Ÿฌ

์ฆ์ƒ: ๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†”์— "Access to fetch has been blocked by CORS policy"

ํ•ด๊ฒฐ:

const cors = require('cors');

app.use(cors({
  origin: 'http://localhost:3000', // ํ”„๋ก ํŠธ์—”๋“œ URL
  credentials: true, // ์ฟ ํ‚ค ํ—ˆ์šฉ
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// Preflight ์š”์ฒญ ์ฒ˜๋ฆฌ
app.options('*', cors());

3. ์ฟ ํ‚ค๊ฐ€ ์ „์†ก๋˜์ง€ ์•Š์Œ

์›์ธ:
โ€ข SameSite ์„ค์ • ๋ฌธ์ œ
โ€ข ๋„๋ฉ”์ธ ๋ถˆ์ผ์น˜
โ€ข credentials ์˜ต์…˜ ๋ˆ„๋ฝ

ํ•ด๊ฒฐ:

// ์„œ๋ฒ„
res.cookie('refreshToken', token, {
  httpOnly: true,
  secure: false, // ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” false
  sameSite: 'lax', // 'strict'๋Š” ๋„ˆ๋ฌด ์—„๊ฒฉํ•  ์ˆ˜ ์žˆ์Œ
  domain: 'localhost' // ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •
});

// ํด๋ผ์ด์–ธํŠธ
fetch('http://localhost:5000/api/profile', {
  credentials: 'include' // ํ•„์ˆ˜!
});

4. ํ† ํฐ ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ๊ฐ€ ์•ˆ ๋จ

๋ฌธ์ œ: ๋งŒ๋ฃŒ๋œ ํ† ํฐ์œผ๋กœ๋„ ๊ณ„์† ์ ‘๊ทผ ๊ฐ€๋Šฅ

์›์ธ: ์„œ๋ฒ„ ์‹œ๊ฐ„๊ณผ ํด๋ผ์ด์–ธํŠธ ์‹œ๊ฐ„ ๋ถˆ์ผ์น˜, ๋˜๋Š” exp ํด๋ ˆ์ž„ ๋ˆ„๋ฝ

ํ•ด๊ฒฐ:

// ํ† ํฐ ์ƒ์„ฑ ์‹œ ๋ช…์‹œ์ ์œผ๋กœ exp ์„ค์ •
const token = jwt.sign(
  { sub: user.id },
  secret,
  { 
    expiresIn: '15m',
    // ๋˜๋Š” ์ง์ ‘ ์„ค์ •
    // exp: Math.floor(Date.now() / 1000) + (15 * 60)
  }
);

// ๊ฒ€์ฆ ์‹œ ์‹œ๊ฐ„ ํ—ˆ์šฉ ์˜ค์ฐจ ์„ค์ • (5์ดˆ)
jwt.verify(token, secret, {
  clockTolerance: 5
});

์œ ์šฉํ•œ ๋””๋ฒ„๊น… ๋„๊ตฌ

๐Ÿ› ๏ธ ์ถ”์ฒœ ๋„๊ตฌ๋“ค

1. jwt.io - JWT ๋””์ฝ”๋”/๊ฒ€์ฆ๊ธฐ (https://jwt.io)
ํ† ํฐ์„ ๋ถ™์—ฌ๋„ฃ์œผ๋ฉด ํ—ค๋”, ํŽ˜์ด๋กœ๋“œ, ์„œ๋ช…์„ ๋ณด์—ฌ์ค˜

2. Postman - API ํ…Œ์ŠคํŒ…
Authorization ํƒญ์—์„œ Bearer Token ์ž๋™ ์„ค์ • ๊ฐ€๋Šฅ

3. Chrome DevTools
โ€ข Application ํƒญ: ์ฟ ํ‚ค, localStorage ํ™•์ธ
โ€ข Network ํƒญ: ์š”์ฒญ ํ—ค๋” ํ™•์ธ

4. VS Code ํ™•์žฅ
โ€ข REST Client: .http ํŒŒ์ผ๋กœ API ํ…Œ์ŠคํŠธ
โ€ข JWT Debugger: ํ† ํฐ ๋””์ฝ”๋”ฉ

๐Ÿ’ผ ์‹ค์ „ ํ”„๋กœ์ ํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค

์ด์ œ ๋ฐฐ์šด ๊ฑธ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ œ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ธ์ฆ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•ด๋ณผ๊ฒŒ. ๐ŸŽจ

ํ”„๋กœ์ ํŠธ: "SkillShare" ์žฌ๋Šฅ ๊ณต์œ  ํ”Œ๋žซํผ

์š”๊ตฌ์‚ฌํ•ญ

โœ… ์†Œ์…œ ๋กœ๊ทธ์ธ (๊ตฌ๊ธ€, ์นด์นด์˜ค)
โœ… ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ๋กœ๊ทธ์ธ
โœ… ์ด๋ฉ”์ผ ์ธ์ฆ
โœ… ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •
โœ… ์—ญํ•  ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด (์‚ฌ์šฉ์ž, ์ „๋ฌธ๊ฐ€, ๊ด€๋ฆฌ์ž)
โœ… ๋‹ค์ค‘ ๋””๋ฐ”์ด์Šค ์„ธ์…˜ ๊ด€๋ฆฌ
โœ… 2๋‹จ๊ณ„ ์ธ์ฆ (์„ ํƒ์‚ฌํ•ญ)

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ

// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true
  },
  password: {
    type: String,
    select: false // ๊ธฐ๋ณธ ์ฟผ๋ฆฌ์—์„œ ์ œ์™ธ
  },
  name: {
    type: String,
    required: true
  },
  avatar: String,
  role: {
    type: String,
    enum: ['user', 'expert', 'admin'],
    default: 'user'
  },
  providers: [{
    provider: String,
    providerId: String,
    email: String,
    connectedAt: Date
  }],
  emailVerified: {
    type: Boolean,
    default: false
  },
  twoFactorEnabled: {
    type: Boolean,
    default: false
  },
  twoFactorSecret: String,
  skills: [String], // ์žฌ๋Šฅ๋„ท์ฒ˜๋Ÿผ ๋ณด์œ  ์Šคํ‚ฌ
  rating: {
    type: Number,
    default: 0
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  
  this.password = await bcrypt.hash(this.password, 12);
  next();
});

// ๋น„๋ฐ€๋ฒˆํ˜ธ ๋น„๊ต ๋ฉ”์„œ๋“œ
userSchema.methods.comparePassword = async function(candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

์ด๋ฉ”์ผ ์ธ์ฆ ๊ตฌํ˜„

// routes/auth.js
const crypto = require('crypto');
const nodemailer = require('nodemailer');

// ์ด๋ฉ”์ผ ์ „์†ก ์„ค์ •
const transporter = nodemailer.createTransport({
  service: 'gmail',
  auth: {
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASSWORD
  }
});

// ํšŒ์›๊ฐ€์ž…
router.post('/register', async (req, res) => {
  const { email, password, name } = req.body;
  
  // ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์‚ฌ์šฉ์ž ํ™•์ธ
  const existingUser = await User.findOne({ email });
  if (existingUser) {
    return res.status(400).json({ error: '์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค' });
  }
  
  // ์‚ฌ์šฉ์ž ์ƒ์„ฑ
  const user = await User.create({
    email,
    password,
    name,
    emailVerified: false
  });
  
  // ์ธ์ฆ ํ† ํฐ ์ƒ์„ฑ (1์‹œ๊ฐ„ ์œ ํšจ)
  const verificationToken = jwt.sign(
    { userId: user.id, type: 'email_verification' },
    process.env.JWT_SECRET,
    { expiresIn: '1h' }
  );
  
  // ์ธ์ฆ ์ด๋ฉ”์ผ ์ „์†ก
  const verificationUrl = `${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`;
  
  await transporter.sendMail({
    from: process.env.EMAIL_USER,
    to: email,
    subject: 'SkillShare ์ด๋ฉ”์ผ ์ธ์ฆ',
    html: `
      <h1>ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!</h1>
      <p>์•„๋ž˜ ๋งํฌ๋ฅผ ํด๋ฆญํ•˜์—ฌ ์ด๋ฉ”์ผ์„ ์ธ์ฆํ•ด์ฃผ์„ธ์š”:</p>
      <a href="${verificationUrl}">์ด๋ฉ”์ผ ์ธ์ฆํ•˜๊ธฐ</a>
      <p>์ด ๋งํฌ๋Š” 1์‹œ๊ฐ„ ๋™์•ˆ ์œ ํšจํ•ฉ๋‹ˆ๋‹ค.</p>
    `
  });
  
  res.status(201).json({
    message: 'ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต! ์ด๋ฉ”์ผ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.',
    userId: user.id
  });
});

// ์ด๋ฉ”์ผ ์ธ์ฆ
router.post('/verify-email', async (req, res) => {
  const { token } = req.body;
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    if (decoded.type !== 'email_verification') {
      return res.status(400).json({ error: '์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค' });
    }
    
    const user = await User.findByIdAndUpdate(
      decoded.userId,
      { emailVerified: true },
      { new: true }
    );
    
    if (!user) {
      return res.status(404).json({ error: '์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค' });
    }
    
    // ์ธ์ฆ ์™„๋ฃŒ ํ›„ ์ž๋™ ๋กœ๊ทธ์ธ
    const accessToken = JWTService.generateAccessToken(user);
    const refreshToken = JWTService.generateRefreshToken(user);
    
    res.cookie('refreshToken', refreshToken, { /* ... */ });
    res.json({
      message: '์ด๋ฉ”์ผ ์ธ์ฆ ์™„๋ฃŒ!',
      accessToken,
      user: {
        id: user.id,
        email: user.email,
        name: user.name
      }
    });
  } catch (error) {
    res.status(400).json({ error: 'ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค' });
  }
});

๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •

// ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์š”์ฒญ
router.post('/forgot-password', async (req, res) => {
  const { email } = req.body;
  
  const user = await User.findOne({ email });
  if (!user) {
    // ๋ณด์•ˆ์ƒ ์‚ฌ์šฉ์ž ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ์•Œ๋ ค์ฃผ์ง€ ์•Š์Œ
    return res.json({ 
      message: '์ด๋ฉ”์ผ์ด ๋ฐœ์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค (์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ)' 
    });
  }
  
  // ์žฌ์„ค์ • ํ† ํฐ ์ƒ์„ฑ (15๋ถ„ ์œ ํšจ)
  const resetToken = jwt.sign(
    { userId: user.id, type: 'password_reset' },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );
  
  const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`;
  
  await transporter.sendMail({
    from: process.env.EMAIL_USER,
    to: email,
    subject: 'SkillShare ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •',
    html: `
      <h1>๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •</h1>
      <p>์•„๋ž˜ ๋งํฌ๋ฅผ ํด๋ฆญํ•˜์—ฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žฌ์„ค์ •ํ•˜์„ธ์š”:</p>
      <a href="${resetUrl}">๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •ํ•˜๊ธฐ</a>
      <p>์ด ๋งํฌ๋Š” 15๋ถ„ ๋™์•ˆ ์œ ํšจํ•ฉ๋‹ˆ๋‹ค.</p>
      <p>์š”์ฒญํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด ์ด ์ด๋ฉ”์ผ์„ ๋ฌด์‹œํ•˜์„ธ์š”.</p>
    `
  });
  
  res.json({ message: '์ด๋ฉ”์ผ์ด ๋ฐœ์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค' });
});

// ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์‹คํ–‰
router.post('/reset-password', async (req, res) => {
  const { token, newPassword } = req.body;
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    if (decoded.type !== 'password_reset') {
      return res.status(400).json({ error: '์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค' });
    }
    
    const user = await User.findById(decoded.userId).select('+password');
    if (!user) {
      return res.status(404).json({ error: '์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค' });
    }
    
    // ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ์„ค์ •
    user.password = newPassword;
    await user.save();
    
    // ๋ชจ๋“  ๊ธฐ์กด ์„ธ์…˜ ๋ฌดํšจํ™”
    await Session.deleteMany({ userId: user.id });
    
    res.json({ message: '๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์žฌ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค' });
  } catch (error) {
    res.status(400).json({ error: 'ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค' });
  }
});

์—ญํ•  ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด (RBAC)

// middleware/rbac.js
const permissions = {
  user: [
    'profile:read',
    'profile:update',
    'skill:search',
    'booking:create'
  ],
  expert: [
    'profile:read',
    'profile:update',
    'skill:search',
    'skill:create',
    'skill:update',
    'booking:create',
    'booking:manage'
  ],
  admin: [
    '*' // ๋ชจ๋“  ๊ถŒํ•œ
  ]
};

function hasPermission(userRole, requiredPermission) {
  const userPermissions = permissions[userRole] || [];
  
  return userPermissions.includes('*') || 
         userPermissions.includes(requiredPermission);
}

function requirePermission(permission) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Unauthorized' });
    }
    
    if (!hasPermission(req.user.role, permission)) {
      return res.status(403).json({ 
        error: 'Forbidden',
        message: `${permission} ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค`
      });
    }
    
    next();
  };
}

module.exports = { requirePermission, hasPermission };
// ์‚ฌ์šฉ ์˜ˆ์‹œ
const { requirePermission } = require('../middleware/rbac');

// ์Šคํ‚ฌ ๋“ฑ๋ก (์ „๋ฌธ๊ฐ€๋งŒ ๊ฐ€๋Šฅ)
router.post('/skills', 
  authenticateJWT,
  requirePermission('skill:create'),
  async (req, res) => {
    const skill = await Skill.create({
      ...req.body,
      expertId: req.userId
    });
    
    res.status(201).json({ skill });
  }
);

// ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ (๊ด€๋ฆฌ์ž๋งŒ ๊ฐ€๋Šฅ)
router.get('/admin/users',
  authenticateJWT,
  requirePermission('user:manage'),
  async (req, res) => {
    const users = await User.find();
    res.json({ users });
  }
);

2๋‹จ๊ณ„ ์ธ์ฆ (2FA) ๊ตฌํ˜„

// 2FA ์„ค์ •
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');

router.post('/2fa/setup', authenticateJWT, async (req, res) => {
  // ์‹œํฌ๋ฆฟ ์ƒ์„ฑ
  const secret = speakeasy.generateSecret({
    name: `SkillShare (${req.user.email})`
  });
  
  // ์‚ฌ์šฉ์ž์—๊ฒŒ ์‹œํฌ๋ฆฟ ์ €์žฅ (์•„์ง ํ™œ์„ฑํ™” ์•ˆ ํ•จ)
  await User.findByIdAndUpdate(req.userId, {
    twoFactorSecret: secret.base32
  });
  
  // QR ์ฝ”๋“œ ์ƒ์„ฑ
  const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
  
  res.json({
    secret: secret.base32,
    qrCode: qrCodeUrl
  });
});

// 2FA ํ™œ์„ฑํ™”
router.post('/2fa/enable', authenticateJWT, async (req, res) => {
  const { token } = req.body;
  
  const user = await User.findById(req.userId);
  
  // ํ† ํฐ ๊ฒ€์ฆ
  const verified = speakeasy.totp.verify({
    secret: user.twoFactorSecret,
    encoding: 'base32',
    token: token
  });
  
  if (!verified) {
    return res.status(400).json({ error: '์œ ํšจํ•˜์ง€ ์•Š์€ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค' });
  }
  
  // 2FA ํ™œ์„ฑํ™”
  await User.findByIdAndUpdate(req.userId, {
    twoFactorEnabled: true
  });
  
  res.json({ message: '2๋‹จ๊ณ„ ์ธ์ฆ์ด ํ™œ์„ฑํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค' });
});

// ๋กœ๊ทธ์ธ ์‹œ 2FA ํ™•์ธ
router.post('/login', async (req, res) => {
  const { email, password, twoFactorToken } = req.body;
  
  const user = await User.findOne({ email }).select('+password');
  
  if (!user || !(await user.comparePassword(password))) {
    return res.status(401).json({ error: '์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค' });
  }
  
  // 2FA ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ
  if (user.twoFactorEnabled) {
    if (!twoFactorToken) {
      return res.status(200).json({ 
        requiresTwoFactor: true,
        userId: user.id
      });
    }
    
    // 2FA ํ† ํฐ ๊ฒ€์ฆ
    const verified = speakeasy.totp.verify({
      secret: user.twoFactorSecret,
      encoding: 'base32',
      token: twoFactorToken
    });
    
    if (!verified) {
      return res.status(401).json({ error: '์œ ํšจํ•˜์ง€ ์•Š์€ 2FA ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค' });
    }
  }
  
  // ๋กœ๊ทธ์ธ ์„ฑ๊ณต
  const accessToken = JWTService.generateAccessToken(user);
  const refreshToken = JWTService.generateRefreshToken(user);
  
  res.cookie('refreshToken', refreshToken, { /* ... */ });
  res.json({ accessToken, user });
});

๐Ÿ“š ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์™”๋‹ค๋ฉด ์ •๋ง ๋Œ€๋‹จํ•ด! ๐ŸŽ‰ OAuth 2.0๊ณผ JWT์— ๋Œ€ํ•ด ์ •๋ง ๋งŽ์€ ๊ฑธ ๋ฐฐ์› ์–ด. ์ด์ œ ๋„ˆ๋Š” ํ˜„๋Œ€์ ์ธ ์ธ์ฆ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋Š” ์ง€์‹์„ ๊ฐ–์ท„์–ด.

๐ŸŽฏ ํ•ต์‹ฌ ์š”์•ฝ

OAuth 2.0:
โ€ข ์ธ๊ฐ€ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ, ๋น„๋ฐ€๋ฒˆํ˜ธ ์—†์ด ์•ˆ์ „ํ•˜๊ฒŒ ๊ถŒํ•œ ๋ถ€์—ฌ
โ€ข 4๊ฐ€์ง€ Grant Type: Authorization Code, Implicit, Password, Client Credentials
โ€ข PKCE๋กœ ๋ชจ๋ฐ”์ผ/SPA ๋ณด์•ˆ ๊ฐ•ํ™”

JWT:
โ€ข Header, Payload, Signature๋กœ ๊ตฌ์„ฑ๋œ ํ† ํฐ
โ€ข Statelessํ•˜์—ฌ ์„œ๋ฒ„ ๋ถ€๋‹ด ๊ฐ์†Œ
โ€ข Access Token (์งง์€ ์ˆ˜๋ช…) + Refresh Token (๊ธด ์ˆ˜๋ช…) ์กฐํ•ฉ

๋ณด์•ˆ:
โ€ข HTTPS ํ•„์ˆ˜, HttpOnly ์ฟ ํ‚ค ์‚ฌ์šฉ
โ€ข XSS, CSRF ๋ฐฉ์–ด
โ€ข Rate Limiting, ๋กœ๊น…, ๋ชจ๋‹ˆํ„ฐ๋ง
โ€ข ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ, ์„ธ์…˜ ๊ด€๋ฆฌ

๐Ÿ’ก ๋‹ค์Œ ๋‹จ๊ณ„๋กœ

1. ์‹ค์Šตํ•˜๊ธฐ: ์ง์ ‘ ์ž‘์€ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ด. ์ด๋ก ๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•ด!
2. ์˜คํ”ˆ์†Œ์Šค ๋ถ„์„: ์œ ๋ช…ํ•œ ํ”„๋กœ์ ํŠธ๋“ค์˜ ์ธ์ฆ ๊ตฌํ˜„์„ ์‚ดํŽด๋ด
3. ์ตœ์‹  ํŠธ๋ Œ๋“œ ํŒ”๋กœ์šฐ: OAuth 2.1, WebAuthn, Passkeys ๋“ฑ ์ƒˆ๋กœ์šด ๊ธฐ์ˆ ๋„ ๊ณต๋ถ€ํ•ด๋ด
4. ๋ณด์•ˆ ๋‰ด์Šค ๊ตฌ๋…: ์ƒˆ๋กœ์šด ์ทจ์•ฝ์ ๊ณผ ๋Œ€์‘ ๋ฐฉ๋ฒ•์„ ๊ณ„์† ํ•™์Šตํ•ด

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ด๋Ÿฐ ์ธ์ฆ ์‹œ์Šคํ…œ์ด ํ•ต์‹ฌ์ด์•ผ. ์‚ฌ์šฉ์ž๋“ค์ด ์•ˆ์ „ํ•˜๊ฒŒ ๋กœ๊ทธ์ธํ•˜๊ณ , ์ž์‹ ์˜ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๊ณ , ๋‹ค๋ฅธ ์‚ฌ๋žŒ์˜ ์žฌ๋Šฅ์„ ์ด์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ธฐ๋ฐ˜์ด ๋ฐ”๋กœ ์ธ์ฆ์ด๊ฑฐ๋“ ! ๐ŸŒŸ

์‘์šฉํ”„๋กœ๊ทธ๋ž˜๋ฐ์—์„œ ์ธ์ฆ์€ ์„ ํƒ์ด ์•„๋‹ˆ๋ผ ํ•„์ˆ˜์•ผ. ํ•˜์ง€๋งŒ ์ด์ œ ๋„ˆ๋Š” ์ด ๋ณต์žกํ•œ ์ฃผ์ œ๋ฅผ ์ •๋ณตํ–ˆ์–ด! ์ž์‹ ๊ฐ์„ ๊ฐ€์ง€๊ณ  ์‹ค์ „์— ์ ์šฉํ•ด๋ด. ์ฒ˜์Œ์—” ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ณ„์† ์—ฐ์Šตํ•˜๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ์ „๋ฌธ๊ฐ€๊ฐ€ ๋˜์–ด ์žˆ์„ ๊ฑฐ์•ผ. ๐Ÿ’ช

๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด, ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ๊ฐœ๋ฐœ์ž๋“ค์€ ์„œ๋กœ ๋•๋Š” ๋ฌธํ™”๊ฐ€ ์žˆ์œผ๋‹ˆ๊นŒ! ๊ทธ๋ฆฌ๊ณ  ๋ณด์•ˆ์€ ๋์ด ์—†๋Š” ์—ฌ์ •์ด๋ผ๋Š” ๊ฑธ ๊ธฐ์–ตํ•ด. ํ•ญ์ƒ ๋ฐฐ์šฐ๊ณ , ๊ฐœ์„ ํ•˜๊ณ , ์—…๋ฐ์ดํŠธํ•˜๋Š” ์ž์„ธ๊ฐ€ ์ค‘์š”ํ•ด.

์ž, ์ด์ œ ๋‚˜๊ฐ€์„œ ๋ฉ‹์ง„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค์–ด๋ด! ํ™”์ดํŒ…! ๐Ÿš€โœจ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0