์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ” OAuth 2.0 ์ธ์ฆ๊ณผ ๋ณด์•ˆ ์œ„ํ˜‘ ์‹œ๋‚˜๋ฆฌ์˜ค

๐Ÿ” OAuth 2.0 ์ธ์ฆ๊ณผ ๋ณด์•ˆ ์œ„ํ˜‘ ์‹œ๋‚˜๋ฆฌ์˜ค

ํ˜„๋Œ€ ์›น์˜ ํ•„์ˆ˜ ์ธ์ฆ ํ”„๋กœํ† ์ฝœ, ๊ทธ ์ด๋ฉด์˜ ๋ณด์•ˆ ์œ„ํ˜‘๊นŒ์ง€ ํŒŒํ—ค์ณ๋ณด์ž!

๐ŸŽฏ OAuth 2.0, ๋„ˆ ๋ˆ„๊ตฌ๋ƒ?

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

2012๋…„์— ์ •์‹์œผ๋กœ ํ‘œ์ค€ํ™”๋œ OAuth 2.0์€ ์ด์ „ ๋ฒ„์ „์ธ OAuth 1.0์˜ ๋ณต์žกํ•จ์„ ๋Œ€ํญ ๊ฐœ์„ ํ–ˆ์–ด. ์ธ์ฆ(Authentication)๊ณผ ์ธ๊ฐ€(Authorization)๋ฅผ ๋ถ„๋ฆฌํ•ด์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ํ•ต์‹ฌ์ด์•ผ.

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

๐Ÿ’ก ์žฌ๋Šฅ๋„ท TIP ์žฌ๋Šฅ๋„ท(https://www.jaenung.net)์—์„œ๋„ ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ ค๋Š” ๊ฐœ๋ฐœ์ž๋“ค์ด ๋งŽ์•„. OAuth 2.0์„ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์ง€!
OAuth 2.0 ์ธ์ฆ ํ๋ฆ„ ์‚ฌ์šฉ์ž (Resource Owner) ํด๋ผ์ด์–ธํŠธ (์• ํ”Œ๋ฆฌ์ผ€์ด์…˜) ์ธ์ฆ ์„œ๋ฒ„ (Authorization Server) ๋ฆฌ์†Œ์Šค ์„œ๋ฒ„ (Resource Server) โ‘  ์ธ์ฆ ์š”์ฒญ โ‘ก ๊ถŒํ•œ ๋ถ€์—ฌ โ‘ข ์•ก์„ธ์Šค ํ† ํฐ โ‘ฃ ํ† ํฐ์œผ๋กœ ๋ฆฌ์†Œ์Šค ์š”์ฒญ

๐Ÿ—๏ธ OAuth 2.0์˜ ํ•ต์‹ฌ ๊ตฌ์„ฑ ์š”์†Œ

OAuth 2.0์„ ์ดํ•ดํ•˜๋ ค๋ฉด ๋จผ์ € ๋“ฑ์žฅ์ธ๋ฌผ๋“ค์„ ์•Œ์•„์•ผ ํ•ด. ๋งˆ์น˜ ์—ฐ๊ทน ๋ฌด๋Œ€ ์œ„์˜ ๋ฐฐ์šฐ๋“ค์ฒ˜๋Ÿผ, ๊ฐ์ž์˜ ์—ญํ• ์ด ๋ช…ํ™•ํ•˜๊ฒŒ ์ •ํ•ด์ ธ ์žˆ๊ฑฐ๋“ . ๐ŸŽฌ

1๏ธโƒฃ Resource Owner (๋ฆฌ์†Œ์Šค ์†Œ์œ ์ž)

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

2๏ธโƒฃ Client (ํด๋ผ์ด์–ธํŠธ)

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

3๏ธโƒฃ Authorization Server (์ธ์ฆ ์„œ๋ฒ„)

์‚ฌ์šฉ์ž์˜ ์‹ ์›์„ ํ™•์ธํ•˜๊ณ , ์•ก์„ธ์Šค ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜๋Š” ์„œ๋ฒ„์•ผ. ๐ŸŽซ
๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ์˜ ๊ฒฝ์šฐ ๊ตฌ๊ธ€์˜ ์ธ์ฆ ์„œ๋ฒ„๊ฐ€ ์ด ์—ญํ• ์„ ๋‹ด๋‹นํ•ด.
์‚ฌ์šฉ์ž๊ฐ€ "์ด ์•ฑ์ด ๋‚ด ์ •๋ณด์— ์ ‘๊ทผํ•˜๋Š” ๊ฑธ ํ—ˆ๋ฝํ•ฉ๋‹ˆ๋‹ค"๋ผ๊ณ  ๋™์˜ํ•˜๋ฉด, ์ด ์„œ๋ฒ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์•ก์„ธ์Šค ํ† ํฐ์ด๋ผ๋Š” ์ผ์ข…์˜ ์ถœ์ž…์ฆ์„ ๋ฐœ๊ธ‰ํ•ด์ฃผ๋Š” ๊ฑฐ์ง€.

4๏ธโƒฃ Resource Server (๋ฆฌ์†Œ์Šค ์„œ๋ฒ„)

์‹ค์ œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ๋Š” ์„œ๋ฒ„์•ผ. ๐Ÿ—„๏ธ
ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์œ ํšจํ•œ ์•ก์„ธ์Šค ํ† ํฐ์„ ์ œ์‹œํ•˜๋ฉด, ์š”์ฒญํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ œ๊ณตํ•ด์ค˜.
๋งŽ์€ ๊ฒฝ์šฐ ์ธ์ฆ ์„œ๋ฒ„์™€ ๋ฆฌ์†Œ์Šค ์„œ๋ฒ„๊ฐ€ ๊ฐ™์€ ์‹œ์Šคํ…œ ๋‚ด์— ์žˆ์ง€๋งŒ, ๋…ผ๋ฆฌ์ ์œผ๋กœ๋Š” ๋ถ„๋ฆฌ๋œ ๊ฐœ๋…์ด์•ผ.

๐Ÿ”‘ ํ•ต์‹ฌ ๊ฐœ๋…: ํ† ํฐ(Token) OAuth 2.0์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฐœ๋…์ด ๋ฐ”๋กœ ํ† ํฐ์ด์•ผ. ํฌ๊ฒŒ ๋‘ ๊ฐ€์ง€ ์ข…๋ฅ˜๊ฐ€ ์žˆ์–ด:

โ€ข Access Token (์•ก์„ธ์Šค ํ† ํฐ): ๋ฆฌ์†Œ์Šค ์„œ๋ฒ„์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๊ถŒํ•œ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฌธ์ž์—ด. ๋ณดํ†ต ์งง์€ ์œ ํšจ๊ธฐ๊ฐ„์„ ๊ฐ€์ ธ.
โ€ข Refresh Token (๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ): ์•ก์„ธ์Šค ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์„ ๋•Œ ์ƒˆ๋กœ์šด ์•ก์„ธ์Šค ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›๊ธฐ ์œ„ํ•œ ํ† ํฐ. ์ƒ๋Œ€์ ์œผ๋กœ ๊ธด ์œ ํšจ๊ธฐ๊ฐ„์„ ๊ฐ€์ ธ.

๐ŸŽญ OAuth 2.0์˜ 4๊ฐ€์ง€ ๊ถŒํ•œ ๋ถ€์—ฌ ๋ฐฉ์‹

OAuth 2.0์€ ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์ธ์ฆ ๋ฐฉ์‹์„ ์ œ๊ณตํ•ด. ๋งˆ์น˜ ์ƒํ™ฉ์— ๋งž๋Š” ์˜ท์„ ๊ณจ๋ผ ์ž…๋“ฏ์ด ๋ง์ด์•ผ. ๐Ÿ‘”
๊ฐ ๋ฐฉ์‹์€ ๋ณด์•ˆ ์ˆ˜์ค€๊ณผ ์‚ฌ์šฉ ํŽธ์˜์„ฑ ์‚ฌ์ด์˜ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๋ฅผ ๊ณ ๋ คํ•ด์„œ ์„ค๊ณ„๋˜์—ˆ์–ด.

๐Ÿ”น Authorization Code Grant (์ธ๊ฐ€ ์ฝ”๋“œ ๋ฐฉ์‹)

๊ฐ€์žฅ ์•ˆ์ „ํ•˜๊ณ  ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋Š” ๋ฐฉ์‹์ด์•ผ. ๐Ÿ›ก๏ธ
์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ฃผ๋กœ ์‚ฌ์šฉ๋˜๋ฉฐ, ์„œ๋ฒ„ ์‚ฌ์ด๋“œ์—์„œ ํด๋ผ์ด์–ธํŠธ ์‹œํฌ๋ฆฟ์„ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด๊ด€ํ•  ์ˆ˜ ์žˆ์„ ๋•Œ ์ ํ•ฉํ•ด.

๐Ÿ“‹ ๋™์ž‘ ์ˆœ์„œ: 1. ์‚ฌ์šฉ์ž๊ฐ€ ํด๋ผ์ด์–ธํŠธ ์•ฑ์—์„œ "๊ตฌ๊ธ€๋กœ ๋กœ๊ทธ์ธ" ๋ฒ„ํŠผ ํด๋ฆญ
2. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์‚ฌ์šฉ์ž๋ฅผ ์ธ์ฆ ์„œ๋ฒ„์˜ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
3. ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•˜๊ณ  ๊ถŒํ•œ ๋ถ€์—ฌ์— ๋™์˜
4. ์ธ์ฆ ์„œ๋ฒ„๊ฐ€ authorization code๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „๋‹ฌ (๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI๋ฅผ ํ†ตํ•ด)
5. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ด ์ฝ”๋“œ์™€ ํด๋ผ์ด์–ธํŠธ ์‹œํฌ๋ฆฟ์„ ์ธ์ฆ ์„œ๋ฒ„์— ์ œ์ถœ
6. ์ธ์ฆ ์„œ๋ฒ„๊ฐ€ ์•ก์„ธ์Šค ํ† ํฐ ๋ฐœ๊ธ‰
7. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์•ก์„ธ์Šค ํ† ํฐ์œผ๋กœ ๋ฆฌ์†Œ์Šค ์„œ๋ฒ„์— ์ ‘๊ทผ

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

// ์ธ๊ฐ€ ์ฝ”๋“œ ์š”์ฒญ ์˜ˆ์‹œ (URL)
https://authorization-server.com/auth?
  response_type=code&
  client_id=YOUR_CLIENT_ID&
  redirect_uri=https://your-app.com/callback&
  scope=read_profile&
  state=random_string_for_csrf_protection

// ํ† ํฐ ๊ตํ™˜ ์š”์ฒญ ์˜ˆ์‹œ (์„œ๋ฒ„ ์‚ฌ์ด๋“œ)
POST /token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=https://your-app.com/callback&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET
๐Ÿ”น Implicit Grant (์•”๋ฌต์  ์Šน์ธ ๋ฐฉ์‹)

์ฃผ๋กœ ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ฐ˜ ์•ฑ์ด๋‚˜ ๋ชจ๋ฐ”์ผ ์•ฑ์—์„œ ์‚ฌ์šฉ๋˜๋˜ ๋ฐฉ์‹์ด์•ผ. ๐Ÿ“ฒ
ํ•˜์ง€๋งŒ ๋ณด์•ˆ ์ทจ์•ฝ์  ๋•Œ๋ฌธ์— ํ˜„์žฌ๋Š” ์‚ฌ์šฉ์ด ๊ถŒ์žฅ๋˜์ง€ ์•Š์•„. ๋Œ€์‹  PKCE๋ฅผ ์‚ฌ์šฉํ•œ Authorization Code ๋ฐฉ์‹์„ ์“ฐ๋Š” ๊ฒŒ ์ข‹์•„.

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ Implicit Grant๋Š” ์•ก์„ธ์Šค ํ† ํฐ์ด URL ํ”„๋ž˜๊ทธ๋จผํŠธ๋ฅผ ํ†ตํ•ด ์ง์ ‘ ์ „๋‹ฌ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ธŒ๋ผ์šฐ์ € ํžˆ์Šคํ† ๋ฆฌ๋‚˜ ๋กœ๊ทธ์— ๋…ธ์ถœ๋  ์œ„ํ—˜์ด ์žˆ์–ด. OAuth 2.0 ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€(RFC 8252)์—์„œ๋Š” ์ด ๋ฐฉ์‹์„ ๋” ์ด์ƒ ๊ถŒ์žฅํ•˜์ง€ ์•Š์•„.
๐Ÿ”น Resource Owner Password Credentials Grant (๋ฆฌ์†Œ์Šค ์†Œ์œ ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž๊ฒฉ์ฆ๋ช… ๋ฐฉ์‹)

์‚ฌ์šฉ์ž๊ฐ€ ํด๋ผ์ด์–ธํŠธ ์•ฑ์— ์ง์ ‘ ์•„์ด๋””์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜๋Š” ๋ฐฉ์‹์ด์•ผ. ๐Ÿ”‘
์ด๊ฑด ํด๋ผ์ด์–ธํŠธ๋ฅผ ์™„์ „ํžˆ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ์„ ๋•Œ๋งŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ด. ์˜ˆ๋ฅผ ๋“ค์–ด, ์šด์˜์ฒด์ œ ์ž์ฒด ์•ฑ์ด๋‚˜ ๊ฐ™์€ ํšŒ์‚ฌ์—์„œ ๋งŒ๋“  ๊ณต์‹ ์•ฑ ๊ฐ™์€ ๊ฒฝ์šฐ์ง€.

// ํ† ํฐ ์š”์ฒญ ์˜ˆ์‹œ
POST /token HTTP/1.1
Host: authorization-server.com
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๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•  ๋•Œ๋งŒ ์ž„์‹œ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„.
๐Ÿ”น Client Credentials Grant (ํด๋ผ์ด์–ธํŠธ ์ž๊ฒฉ์ฆ๋ช… ๋ฐฉ์‹)

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

// ํ† ํฐ ์š”์ฒญ ์˜ˆ์‹œ
POST /token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
scope=api_access

์ด ๋ฐฉ์‹์€ ์‚ฌ์šฉ์ž ์ปจํ…์ŠคํŠธ๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ๋ฐœ๊ธ‰๋˜์ง€ ์•Š์•„. ์•ก์„ธ์Šค ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜๋ฉด ๋‹ค์‹œ ์š”์ฒญํ•˜๋ฉด ๋ผ.

๐Ÿ” OAuth 2.0์˜ ๋ณด์•ˆ ๊ฐ•ํ™” ๊ธฐ์ˆ : PKCE

๋ชจ๋ฐ”์ผ ์•ฑ์ด๋‚˜ SPA(Single Page Application)์—์„œ Authorization Code ๋ฐฉ์‹์„ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด PKCE(Proof Key for Code Exchange)๊ฐ€ ๋“ฑ์žฅํ–ˆ์–ด. ๐Ÿ”
๋ฐœ์Œ์€ "ํ”ฝ์‹œ"๋ผ๊ณ  ํ•ด. ๊ท€์—ฝ์ง€? ํ•˜์ง€๋งŒ ๋ณด์•ˆ์€ ๊ฐ•๋ ฅํ•ด!

โœจ PKCE์˜ ํ•ต์‹ฌ ์•„์ด๋””์–ด ํด๋ผ์ด์–ธํŠธ ์‹œํฌ๋ฆฟ์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅํ•  ์ˆ˜ ์—†๋Š” ํ™˜๊ฒฝ(๋ชจ๋ฐ”์ผ ์•ฑ, SPA ๋“ฑ)์—์„œ๋„ ์ธ๊ฐ€ ์ฝ”๋“œ ๊ฐ€๋กœ์ฑ„๊ธฐ ๊ณต๊ฒฉ์„ ๋ฐฉ์–ดํ•  ์ˆ˜ ์žˆ๋„๋ก ๋™์ ์œผ๋กœ ์ƒ์„ฑ๋œ ์‹œํฌ๋ฆฟ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฑฐ์•ผ.

PKCE ๋™์ž‘ ๋ฐฉ์‹: โš™๏ธ

1. ํด๋ผ์ด์–ธํŠธ๊ฐ€ code_verifier๋ผ๋Š” ๋žœ๋ค ๋ฌธ์ž์—ด ์ƒ์„ฑ (43~128์ž)
2. ์ด๋ฅผ SHA256์œผ๋กœ ํ•ด์‹ฑํ•˜์—ฌ code_challenge ์ƒ์„ฑ
3. ์ธ๊ฐ€ ์š”์ฒญ ์‹œ code_challenge๋ฅผ ํ•จ๊ป˜ ์ „์†ก
4. ์ธ์ฆ ์„œ๋ฒ„๊ฐ€ code_challenge๋ฅผ ์ €์žฅ
5. ํ† ํฐ ๊ตํ™˜ ์‹œ ์›๋ณธ code_verifier๋ฅผ ํ•จ๊ป˜ ์ „์†ก
6. ์ธ์ฆ ์„œ๋ฒ„๊ฐ€ code_verifier๋ฅผ ํ•ด์‹ฑํ•˜์—ฌ ์ €์žฅ๋œ code_challenge์™€ ๋น„๊ต
7. ์ผ์น˜ํ•˜๋ฉด ํ† ํฐ ๋ฐœ๊ธ‰
// PKCE๋ฅผ ์‚ฌ์šฉํ•œ ์ธ๊ฐ€ ์š”์ฒญ
https://authorization-server.com/auth?
  response_type=code&
  client_id=YOUR_CLIENT_ID&
  redirect_uri=https://your-app.com/callback&
  scope=read_profile&
  state=random_string&
  code_challenge=HASHED_CODE_VERIFIER&
  code_challenge_method=S256

// ํ† ํฐ ๊ตํ™˜ ์‹œ
POST /token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=https://your-app.com/callback&
client_id=YOUR_CLIENT_ID&
code_verifier=ORIGINAL_RANDOM_STRING

๊ณต๊ฒฉ์ž๊ฐ€ ์ธ๊ฐ€ ์ฝ”๋“œ๋ฅผ ๊ฐ€๋กœ์ฑˆ๋‹ค ํ•ด๋„, code_verifier๋ฅผ ๋ชจ๋ฅด๋ฉด ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›์„ ์ˆ˜ ์—†์–ด. ๐Ÿ›ก๏ธ
2021๋…„ OAuth 2.1 ๋“œ๋ž˜ํ”„ํŠธ์—์„œ๋Š” PKCE๋ฅผ ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์— ํ•„์ˆ˜๋กœ ์ ์šฉํ•˜๋„๋ก ๊ถŒ์žฅํ•˜๊ณ  ์žˆ์–ด.

โš ๏ธ OAuth 2.0์˜ ์ฃผ์š” ๋ณด์•ˆ ์œ„ํ˜‘ ์‹œ๋‚˜๋ฆฌ์˜ค

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์–ด๋‘์šด ๋ฉด์„ ๋“ค์—ฌ๋‹ค๋ณผ ์‹œ๊ฐ„์ด์•ผ. ๐Ÿ•ต๏ธ
OAuth 2.0์€ ๊ฐ•๋ ฅํ•˜์ง€๋งŒ, ์ž˜๋ชป ๊ตฌํ˜„ํ•˜๋ฉด ์‹ฌ๊ฐํ•œ ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ์ƒ๊ธธ ์ˆ˜ ์žˆ์–ด. ์‹ค์ œ๋กœ ๋งŽ์€ ์„œ๋น„์Šค๋“ค์ด ์ด๋Ÿฐ ๋ฌธ์ œ๋กœ ํ•ดํ‚น๋‹นํ–ˆ๊ฑฐ๋“ .

๐ŸŽฏ 1. Authorization Code Interception (์ธ๊ฐ€ ์ฝ”๋“œ ๊ฐ€๋กœ์ฑ„๊ธฐ)

๊ณต๊ฒฉ์ž๊ฐ€ ์‚ฌ์šฉ์ž์™€ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด์˜ ํ†ต์‹ ์„ ๊ฐ€๋กœ์ฑ„์„œ ์ธ๊ฐ€ ์ฝ”๋“œ๋ฅผ ํ›”์น˜๋Š” ๊ณต๊ฒฉ์ด์•ผ. ๐ŸŽฃ

๐Ÿ”ด ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค: 1. ์‚ฌ์šฉ์ž๊ฐ€ ์ •์ƒ์ ์œผ๋กœ OAuth ์ธ์ฆ ์ง„ํ–‰
2. ์ธ์ฆ ์„œ๋ฒ„๊ฐ€ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI๋กœ ์ธ๊ฐ€ ์ฝ”๋“œ ์ „์†ก
3. ๊ณต๊ฒฉ์ž๊ฐ€ ์ค‘๊ฐ„์—์„œ ์ด ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ฅผ ๊ฐ€๋กœ์ฑ” (์˜ˆ: ์•…์„ฑ ์•ฑ์ด ์ปค์Šคํ…€ URL ์Šคํ‚ด ๊ฐ€๋กœ์ฑ„๊ธฐ)
4. ๊ณต๊ฒฉ์ž๊ฐ€ ํ›”์นœ ์ธ๊ฐ€ ์ฝ”๋“œ๋กœ ์•ก์„ธ์Šค ํ† ํฐ ๋ฐœ๊ธ‰ ์‹œ๋„
5. ์„ฑ๊ณตํ•˜๋ฉด ์‚ฌ์šฉ์ž์˜ ๋ฆฌ์†Œ์Šค์— ๋ฌด๋‹จ ์ ‘๊ทผ

๋ฐฉ์–ด ๋ฐฉ๋ฒ•: ๐Ÿ›ก๏ธ

โ€ข PKCE ์‚ฌ์šฉ: ๊ฐ€์žฅ ํšจ๊ณผ์ ์ธ ๋ฐฉ์–ด์ฑ…. code_verifier ์—†์ด๋Š” ํ† ํฐ ๋ฐœ๊ธ‰ ๋ถˆ๊ฐ€
โ€ข State ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ: CSRF ๊ณต๊ฒฉ ๋ฐฉ์ง€ ๋ฐ ์š”์ฒญ ๋ฌด๊ฒฐ์„ฑ ํ™•์ธ
โ€ข ์งง์€ ์ธ๊ฐ€ ์ฝ”๋“œ ์œ ํšจ๊ธฐ๊ฐ„: ๋ณดํ†ต 10๋ถ„ ์ด๋‚ด๋กœ ์„ค์ •
โ€ข ์ผํšŒ์šฉ ์ธ๊ฐ€ ์ฝ”๋“œ: ํ•œ ๋ฒˆ ์‚ฌ์šฉ๋œ ์ฝ”๋“œ๋Š” ์ฆ‰์‹œ ๋ฌดํšจํ™”
โ€ข HTTPS ํ•„์ˆ˜ ์‚ฌ์šฉ: ๋ชจ๋“  ํ†ต์‹ ์„ ์•”ํ˜ธํ™”
๐ŸŽฏ 2. Redirect URI Manipulation (๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ์กฐ์ž‘)

OAuth์˜ ๊ฐ€์žฅ ์น˜๋ช…์ ์ธ ์ทจ์•ฝ์  ์ค‘ ํ•˜๋‚˜์•ผ. ๐Ÿ’€
๊ณต๊ฒฉ์ž๊ฐ€ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI๋ฅผ ์ž์‹ ์˜ ์„œ๋ฒ„๋กœ ๋ณ€๊ฒฝํ•ด์„œ ์ธ๊ฐ€ ์ฝ”๋“œ๋‚˜ ํ† ํฐ์„ ๊ฐ€๋กœ์ฑ„๋Š” ๊ณต๊ฒฉ์ด์ง€.

๐Ÿ”ด ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค: ์‹œ๋‚˜๋ฆฌ์˜ค A - Open Redirect ์ทจ์•ฝ์ :
1. ๊ณต๊ฒฉ์ž๊ฐ€ ์กฐ์ž‘๋œ ์ธ๊ฐ€ ์š”์ฒญ URL ์ƒ์„ฑ
redirect_uri=https://attacker.com/steal
2. ์‚ฌ์šฉ์ž๋ฅผ ํ”ผ์‹ฑ ๋งํฌ๋กœ ์œ ๋„
3. ์‚ฌ์šฉ์ž๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋กœ๊ทธ์ธํ•˜๊ณ  ๊ถŒํ•œ ๋ถ€์—ฌ
4. ์ธ๊ฐ€ ์ฝ”๋“œ๊ฐ€ ๊ณต๊ฒฉ์ž์˜ ์„œ๋ฒ„๋กœ ์ „์†ก๋จ

์‹œ๋‚˜๋ฆฌ์˜ค B - Path Traversal:
์ •์ƒ URI: https://client.com/callback
์กฐ์ž‘ URI: https://client.com/callback/../../../attacker-page
๋˜๋Š”: https://client.com/callback?redirect=https://attacker.com

์‹ค์ œ ์‚ฌ๋ก€: ๐Ÿ“ฐ
2014๋…„, ํŽ˜์ด์Šค๋ถ์˜ OAuth ๊ตฌํ˜„์—์„œ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ๊ฒ€์ฆ ์ทจ์•ฝ์ ์ด ๋ฐœ๊ฒฌ๋˜์—ˆ์–ด. ๊ณต๊ฒฉ์ž๊ฐ€ ํŠน์ • ๋„๋ฉ”์ธ์˜ ์„œ๋ธŒ๋„๋ฉ”์ธ์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•  ์ˆ˜ ์žˆ์—ˆ๊ณ , ์ด๋ฅผ ํ†ตํ•ด ์•ก์„ธ์Šค ํ† ํฐ์„ ํƒˆ์ทจํ•  ์ˆ˜ ์žˆ์—ˆ์ง€.

๋ฐฉ์–ด ๋ฐฉ๋ฒ•: ๐Ÿ›ก๏ธ

โ€ข ์ •ํ™•ํ•œ URI ๋งค์นญ: ์™€์ผ๋“œ์นด๋“œ๋‚˜ ๋ถ€๋ถ„ ๋งค์นญ ๊ธˆ์ง€. ๋“ฑ๋ก๋œ URI์™€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•ด์•ผ ํ•จ
โ€ข ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๋ฐฉ์‹: ํ—ˆ์šฉ๋œ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ๋ชฉ๋ก์„ ์‚ฌ์ „์— ๋“ฑ๋ก
โ€ข ํ”„๋กœํ† ์ฝœ ๊ฒ€์ฆ: HTTPS๋งŒ ํ—ˆ์šฉ (๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์ œ์™ธ)
โ€ข ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ: ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI์— ์ถ”๊ฐ€ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋ถ™๋Š” ๊ฒƒ ์ œํ•œ
โ€ข ์ •๊ทœํ™” ํ›„ ๋น„๊ต: URL ์ธ์ฝ”๋”ฉ, ๋Œ€์†Œ๋ฌธ์ž ๋“ฑ์„ ์ •๊ทœํ™”ํ•œ ํ›„ ๋น„๊ต
// ์•ˆ์ „ํ•œ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ๊ฒ€์ฆ ์˜ˆ์‹œ (Python)
from urllib.parse import urlparse

ALLOWED_REDIRECT_URIS = [
    'https://myapp.com/callback',
    'https://myapp.com/oauth/callback'
]

def validate_redirect_uri(redirect_uri):
    # ์ •ํ™•ํ•œ ๋งค์นญ๋งŒ ํ—ˆ์šฉ
    if redirect_uri not in ALLOWED_REDIRECT_URIS:
        raise ValueError("Invalid redirect URI")
    
    # ์ถ”๊ฐ€ ๋ณด์•ˆ ๊ฒ€์ฆ
    parsed = urlparse(redirect_uri)
    if parsed.scheme != 'https':
        raise ValueError("Only HTTPS allowed")
    
    return True
๐ŸŽฏ 3. Token Leakage (ํ† ํฐ ์œ ์ถœ)

์•ก์„ธ์Šค ํ† ํฐ์ด๋‚˜ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ์˜๋„์น˜ ์•Š๊ฒŒ ์™ธ๋ถ€์— ๋…ธ์ถœ๋˜๋Š” ๊ฒฝ์šฐ์•ผ. ๐Ÿ”“
์ƒ๊ฐ๋ณด๋‹ค ๋‹ค์–‘ํ•œ ๊ฒฝ๋กœ๋กœ ํ† ํฐ์ด ์œ ์ถœ๋  ์ˆ˜ ์žˆ์–ด.

๐Ÿ”ด ํ† ํฐ ์œ ์ถœ ๊ฒฝ๋กœ: 1. ๋ธŒ๋ผ์šฐ์ € ํžˆ์Šคํ† ๋ฆฌ:
Implicit Grant์—์„œ ํ† ํฐ์ด URL ํ”„๋ž˜๊ทธ๋จผํŠธ์— ํฌํ•จ๋˜์–ด ๋ธŒ๋ผ์šฐ์ € ํžˆ์Šคํ† ๋ฆฌ์— ์ €์žฅ

2. Referer ํ—ค๋”:
ํ† ํฐ์ด ํฌํ•จ๋œ ํŽ˜์ด์ง€์—์„œ ์™ธ๋ถ€ ๋ฆฌ์†Œ์Šค(์ด๋ฏธ์ง€, ์Šคํฌ๋ฆฝํŠธ ๋“ฑ) ๋กœ๋“œ ์‹œ Referer ํ—ค๋”๋กœ ์œ ์ถœ

3. ๋กœ๊ทธ ํŒŒ์ผ:
์„œ๋ฒ„ ๋กœ๊ทธ, ํ”„๋ก์‹œ ๋กœ๊ทธ, CDN ๋กœ๊ทธ ๋“ฑ์— ํ† ํฐ์ด ๊ธฐ๋ก๋จ

4. ํด๋ผ์ด์–ธํŠธ ์ €์žฅ์†Œ:
LocalStorage, SessionStorage์— ์ €์žฅ๋œ ํ† ํฐ์ด XSS ๊ณต๊ฒฉ์œผ๋กœ ํƒˆ์ทจ

5. ๋„คํŠธ์›Œํฌ ์Šค๋‹ˆํ•‘:
HTTPS๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ์ธ์ฆ์„œ ๊ฒ€์ฆ์„ ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ

์‹ค์ œ ์‚ฌ๋ก€: ๐Ÿ“ฐ
2018๋…„, ํ•œ ๋Œ€ํ˜• ์†Œ์…œ ๋ฏธ๋””์–ด ํ”Œ๋žซํผ์—์„œ ์•ก์„ธ์Šค ํ† ํฐ์ด ์„œ๋ฒ„ ๋กœ๊ทธ์— ํ‰๋ฌธ์œผ๋กœ ๊ธฐ๋ก๋˜๋Š” ๋ฒ„๊ทธ๊ฐ€ ๋ฐœ๊ฒฌ๋˜์—ˆ์–ด. ์ˆ˜๋ฐฑ๋งŒ ๋ช…์˜ ์‚ฌ์šฉ์ž ํ† ํฐ์ด ๋‚ด๋ถ€ ์‹œ์Šคํ…œ์— ๋…ธ์ถœ๋˜์—ˆ์ง€.

๋ฐฉ์–ด ๋ฐฉ๋ฒ•: ๐Ÿ›ก๏ธ

โ€ข HttpOnly ์ฟ ํ‚ค ์‚ฌ์šฉ: JavaScript์—์„œ ์ ‘๊ทผ ๋ถˆ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ† ํฐ ์ €์žฅ
โ€ข ์งง์€ ํ† ํฐ ์œ ํšจ๊ธฐ๊ฐ„: ์•ก์„ธ์Šค ํ† ํฐ์€ 15๋ถ„~1์‹œ๊ฐ„ ์ •๋„๋กœ ์„ค์ •
โ€ข Secure ํ”Œ๋ž˜๊ทธ: ์ฟ ํ‚ค๊ฐ€ HTTPS์—์„œ๋งŒ ์ „์†ก๋˜๋„๋ก ์„ค์ •
โ€ข SameSite ์†์„ฑ: CSRF ๊ณต๊ฒฉ ๋ฐฉ์–ด
โ€ข ํ† ํฐ ๋กœ๊น… ๊ธˆ์ง€: ๋กœ๊ทธ์— ํ† ํฐ์ด ๊ธฐ๋ก๋˜์ง€ ์•Š๋„๋ก ํ•„ํ„ฐ๋ง
โ€ข Referrer-Policy ํ—ค๋”: Referer ํ—ค๋” ์ „์†ก ์ œํ•œ
โ€ข ํ† ํฐ ์•”ํ˜ธํ™”: ์ €์žฅ ์‹œ ์•”ํ˜ธํ™”, ์‚ฌ์šฉ ์‹œ ๋ณตํ˜ธํ™”
// ์•ˆ์ „ํ•œ ํ† ํฐ ์ €์žฅ ์˜ˆ์‹œ (JavaScript)
// โŒ ๋‚˜์œ ์˜ˆ: LocalStorage ์‚ฌ์šฉ
localStorage.setItem('access_token', token); // XSS์— ์ทจ์•ฝ!

// โœ… ์ข‹์€ ์˜ˆ: HttpOnly ์ฟ ํ‚ค ์‚ฌ์šฉ (์„œ๋ฒ„ ์‚ฌ์ด๋“œ)
Set-Cookie: access_token=xxx; HttpOnly; Secure; SameSite=Strict; Max-Age=3600

// ๋˜๋Š” ๋ฉ”๋ชจ๋ฆฌ์—๋งŒ ์ €์žฅ (SPA์˜ ๊ฒฝ์šฐ)
class TokenManager {
    constructor() {
        this._token = null; // ํด๋กœ์ €๋กœ ๋ณดํ˜ธ
    }
    
    setToken(token) {
        this._token = token;
    }
    
    getToken() {
        return this._token;
    }
}
๐ŸŽฏ 4. CSRF (Cross-Site Request Forgery) ๊ณต๊ฒฉ

๊ณต๊ฒฉ์ž๊ฐ€ ์‚ฌ์šฉ์ž์˜ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ด์šฉํ•ด ์˜๋„ํ•˜์ง€ ์•Š์€ OAuth ์ธ์ฆ์„ ์ˆ˜ํ–‰ํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๊ณต๊ฒฉ์ด์•ผ. ๐ŸŽญ

๐Ÿ”ด ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค: 1. ๊ณต๊ฒฉ์ž๊ฐ€ ์ž์‹ ์˜ ๊ณ„์ •์œผ๋กœ OAuth ์ธ์ฆ ์‹œ์ž‘
2. ์ธ๊ฐ€ ์ฝ”๋“œ๋ฅผ ๋ฐ›๋Š” ๋‹จ๊ณ„์—์„œ ํ”„๋กœ์„ธ์Šค ์ค‘๋‹จ
3. ํ”ผํ•ด์ž๋ฅผ ์•…์„ฑ ํŽ˜์ด์ง€๋กœ ์œ ๋„
4. ์•…์„ฑ ํŽ˜์ด์ง€๊ฐ€ ์ž๋™์œผ๋กœ ๊ณต๊ฒฉ์ž์˜ ์ธ๊ฐ€ ์ฝ”๋“œ๋กœ ํ† ํฐ ๊ตํ™˜ ์š”์ฒญ ์ „์†ก
5. ํ”ผํ•ด์ž์˜ ์„ธ์…˜์— ๊ณต๊ฒฉ์ž์˜ ๊ณ„์ •์ด ์—ฐ๊ฒฐ๋จ
6. ํ”ผํ•ด์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ •๋ณด(๊ฒฐ์ œ ์ •๋ณด, ๊ฐœ์ธ์ •๋ณด ๋“ฑ)๊ฐ€ ๊ณต๊ฒฉ์ž์˜ ๊ณ„์ •์— ์ €์žฅ๋จ

๋ฐฉ์–ด ๋ฐฉ๋ฒ•: ๐Ÿ›ก๏ธ

โ€ข State ํŒŒ๋ผ๋ฏธํ„ฐ ํ•„์ˆ˜ ์‚ฌ์šฉ: ์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅํ•œ ๋žœ๋ค ๊ฐ’ ์ƒ์„ฑ ๋ฐ ๊ฒ€์ฆ
โ€ข ์„ธ์…˜ ๋ฐ”์ธ๋”ฉ: State ๊ฐ’์„ ์„œ๋ฒ„ ์„ธ์…˜์— ์ €์žฅํ•˜๊ณ  ๊ฒ€์ฆ
โ€ข Nonce ์‚ฌ์šฉ: OpenID Connect์˜ ๊ฒฝ์šฐ nonce ํŒŒ๋ผ๋ฏธํ„ฐ ํ™œ์šฉ
โ€ข ์งง์€ ์œ ํšจ๊ธฐ๊ฐ„: State ๊ฐ’์— ํƒ€์ž„์Šคํƒฌํ”„ ํฌํ•จ ๋ฐ ๊ฒ€์ฆ
// State ํŒŒ๋ผ๋ฏธํ„ฐ ์ƒ์„ฑ ๋ฐ ๊ฒ€์ฆ ์˜ˆ์‹œ (Node.js)
const crypto = require('crypto');

// ์ธ๊ฐ€ ์š”์ฒญ ์‹œ
function generateAuthUrl() {
    const state = crypto.randomBytes(32).toString('hex');
    
    // ์„ธ์…˜์— ์ €์žฅ
    req.session.oauthState = state;
    
    const authUrl = `https://authorization-server.com/auth?` +
        `response_type=code&` +
        `client_id=${CLIENT_ID}&` +
        `redirect_uri=${REDIRECT_URI}&` +
        `state=${state}`;
    
    return authUrl;
}

// ์ฝœ๋ฐฑ ์ฒ˜๋ฆฌ ์‹œ
function handleCallback(req, res) {
    const receivedState = req.query.state;
    const savedState = req.session.oauthState;
    
    // State ๊ฒ€์ฆ
    if (!receivedState || receivedState !== savedState) {
        throw new Error('Invalid state parameter - possible CSRF attack');
    }
    
    // State ์ผํšŒ์šฉ์œผ๋กœ ๋งŒ๋“ค๊ธฐ
    delete req.session.oauthState;
    
    // ํ† ํฐ ๊ตํ™˜ ์ง„ํ–‰...
}
๐ŸŽฏ 5. Client Impersonation (ํด๋ผ์ด์–ธํŠธ ์œ„์žฅ)

๊ณต๊ฒฉ์ž๊ฐ€ ์ •์ƒ ํด๋ผ์ด์–ธํŠธ์ธ ์ฒ™ ์œ„์žฅํ•ด์„œ ์‚ฌ์šฉ์ž๋ฅผ ์†์ด๋Š” ๊ณต๊ฒฉ์ด์•ผ. ๐ŸŽช

๐Ÿ”ด ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค: ์‹œ๋‚˜๋ฆฌ์˜ค A - ํด๋ผ์ด์–ธํŠธ ID ๋„์šฉ:
1. ๊ณต๊ฒฉ์ž๊ฐ€ ์ •์ƒ ์•ฑ์˜ ํด๋ผ์ด์–ธํŠธ ID๋ฅผ ์ถ”์ถœ (๋””์ปดํŒŒ์ผ, ๋„คํŠธ์›Œํฌ ๋ถ„์„ ๋“ฑ)
2. ์•…์„ฑ ์•ฑ์— ๋™์ผํ•œ ํด๋ผ์ด์–ธํŠธ ID ์‚ฌ์šฉ
3. ์‚ฌ์šฉ์ž๊ฐ€ ์ •์ƒ ์•ฑ์œผ๋กœ ์ฐฉ๊ฐํ•˜๊ณ  ๊ถŒํ•œ ๋ถ€์—ฌ
4. ๊ณต๊ฒฉ์ž๊ฐ€ ์‚ฌ์šฉ์ž์˜ ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผ

์‹œ๋‚˜๋ฆฌ์˜ค B - ํ”ผ์‹ฑ ์•ฑ:
1. ๊ณต๊ฒฉ์ž๊ฐ€ ์ •์ƒ ์•ฑ๊ณผ ์œ ์‚ฌํ•œ ์ด๋ฆ„/์•„์ด์ฝ˜์˜ ์•…์„ฑ ์•ฑ ๋ฐฐํฌ
2. ์‚ฌ์šฉ์ž๊ฐ€ ์•…์„ฑ ์•ฑ์—์„œ OAuth ์ธ์ฆ ์ง„ํ–‰
3. ์•…์„ฑ ์•ฑ์ด ๊ณผ๋„ํ•œ ๊ถŒํ•œ ์š”์ฒญ
4. ์‚ฌ์šฉ์ž ์ •๋ณด ํƒˆ์ทจ

๋ฐฉ์–ด ๋ฐฉ๋ฒ•: ๐Ÿ›ก๏ธ

โ€ข ๋™์  ํด๋ผ์ด์–ธํŠธ ๋“ฑ๋ก: ๋Ÿฐํƒ€์ž„์— ํด๋ผ์ด์–ธํŠธ ์ž๊ฒฉ์ฆ๋ช… ์ƒ์„ฑ
โ€ข ํด๋ผ์ด์–ธํŠธ ์ธ์ฆ ๊ฐ•ํ™”: ํด๋ผ์ด์–ธํŠธ ์‹œํฌ๋ฆฟ, ์ธ์ฆ์„œ ๊ธฐ๋ฐ˜ ์ธ์ฆ ๋“ฑ
โ€ข ์•ฑ ์„œ๋ช… ๊ฒ€์ฆ: ๋ชจ๋ฐ”์ผ ์•ฑ์˜ ๊ฒฝ์šฐ ์•ฑ ์„œ๋ช… ํ™•์ธ
โ€ข ์‚ฌ์šฉ์ž ๋™์˜ ํ™”๋ฉด ๊ฐœ์„ : ์–ด๋–ค ์•ฑ์ด ์–ด๋–ค ๊ถŒํ•œ์„ ์š”์ฒญํ•˜๋Š”์ง€ ๋ช…ํ™•ํžˆ ํ‘œ์‹œ
โ€ข ๊ถŒํ•œ ์ตœ์†Œํ™” ์›์น™: ํ•„์š”ํ•œ ์ตœ์†Œํ•œ์˜ ์Šค์ฝ”ํ”„๋งŒ ์š”์ฒญ
โ€ข ์•ฑ ๊ฒ€์ฆ ํ”„๋กœ์„ธ์Šค: ์•ฑ ์Šคํ† ์–ด ๋“ฑ๋ก ์‹œ ์ฒ ์ €ํ•œ ๊ฒ€์ฆ
๐ŸŽฏ 6. Token Replay Attack (ํ† ํฐ ์žฌ์‚ฌ์šฉ ๊ณต๊ฒฉ)

๊ณต๊ฒฉ์ž๊ฐ€ ํƒˆ์ทจํ•œ ํ† ํฐ์„ ๋ฐ˜๋ณต์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ณต๊ฒฉ์ด์•ผ. ๐Ÿ”

๐Ÿ”ด ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค: 1. ๊ณต๊ฒฉ์ž๊ฐ€ ๋„คํŠธ์›Œํฌ ์Šค๋‹ˆํ•‘, XSS ๋“ฑ์œผ๋กœ ์•ก์„ธ์Šค ํ† ํฐ ํƒˆ์ทจ
2. ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜๊ธฐ ์ „๊นŒ์ง€ ๋ฐ˜๋ณต์ ์œผ๋กœ API ํ˜ธ์ถœ
3. ์‚ฌ์šฉ์ž์˜ ๋ฐ์ดํ„ฐ ์กฐํšŒ, ์ˆ˜์ •, ์‚ญ์ œ ๋“ฑ ์•…์˜์  ํ–‰์œ„ ์ˆ˜ํ–‰
4. ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ๊นŒ์ง€ ํƒˆ์ทจํ•œ ๊ฒฝ์šฐ ์žฅ๊ธฐ๊ฐ„ ์ ‘๊ทผ ๊ฐ€๋Šฅ

๋ฐฉ์–ด ๋ฐฉ๋ฒ•: ๐Ÿ›ก๏ธ

โ€ข ํ† ํฐ ๋ฐ”์ธ๋”ฉ: ํ† ํฐ์„ ํŠน์ • ํด๋ผ์ด์–ธํŠธ๋‚˜ ์„ธ์…˜์— ๋ฐ”์ธ๋”ฉ
โ€ข IP ์ฃผ์†Œ ๊ฒ€์ฆ: ํ† ํฐ ๋ฐœ๊ธ‰ ์‹œ IP์™€ ์‚ฌ์šฉ ์‹œ IP ๋น„๊ต
โ€ข User-Agent ๊ฒ€์ฆ: ๋ธŒ๋ผ์šฐ์ €/๋””๋ฐ”์ด์Šค ์ •๋ณด ํ™•์ธ
โ€ข ์ด์ƒ ํ–‰์œ„ ํƒ์ง€: ๋น„์ •์ƒ์ ์ธ API ํ˜ธ์ถœ ํŒจํ„ด ๋ชจ๋‹ˆํ„ฐ๋ง
โ€ข ํ† ํฐ ํšŒ์ „: ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์‚ฌ์šฉ ์‹œ๋งˆ๋‹ค ์ƒˆ๋กœ์šด ํ† ํฐ ๋ฐœ๊ธ‰
โ€ข ํ† ํฐ ์ทจ์†Œ ๋ฉ”์ปค๋‹ˆ์ฆ˜: ์˜์‹ฌ์Šค๋Ÿฌ์šด ํ™œ๋™ ๊ฐ์ง€ ์‹œ ์ฆ‰์‹œ ํ† ํฐ ๋ฌดํšจํ™”
// ํ† ํฐ ๋ฐ”์ธ๋”ฉ ์˜ˆ์‹œ (JWT ์‚ฌ์šฉ)
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// ํ† ํฐ ๋ฐœ๊ธ‰ ์‹œ
function issueToken(userId, req) {
    // ํด๋ผ์ด์–ธํŠธ ํ•‘๊ฑฐํ”„๋ฆฐํŠธ ์ƒ์„ฑ
    const fingerprint = crypto.createHash('sha256')
        .update(req.ip + req.headers['user-agent'])
        .digest('hex');
    
    const token = jwt.sign(
        {
            sub: userId,
            fingerprint: fingerprint,
            iat: Math.floor(Date.now() / 1000),
            exp: Math.floor(Date.now() / 1000) + 3600
        },
        SECRET_KEY
    );
    
    return token;
}

// ํ† ํฐ ๊ฒ€์ฆ ์‹œ
function verifyToken(token, req) {
    const decoded = jwt.verify(token, SECRET_KEY);
    
    // ํ•‘๊ฑฐํ”„๋ฆฐํŠธ ์žฌ์ƒ์„ฑ ๋ฐ ๋น„๊ต
    const currentFingerprint = crypto.createHash('sha256')
        .update(req.ip + req.headers['user-agent'])
        .digest('hex');
    
    if (decoded.fingerprint !== currentFingerprint) {
        throw new Error('Token binding validation failed');
    }
    
    return decoded;
}
๐ŸŽฏ 7. Scope Manipulation (์Šค์ฝ”ํ”„ ์กฐ์ž‘)

๊ณต๊ฒฉ์ž๊ฐ€ ์š”์ฒญํ•˜๋Š” ๊ถŒํ•œ ๋ฒ”์œ„๋ฅผ ์กฐ์ž‘ํ•ด์„œ ๊ณผ๋„ํ•œ ๊ถŒํ•œ์„ ์–ป์œผ๋ ค๋Š” ๊ณต๊ฒฉ์ด์•ผ. ๐ŸŽš๏ธ

๐Ÿ”ด ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค: 1. ์ •์ƒ์ ์œผ๋กœ๋Š” scope=read_profile๋งŒ ์š”์ฒญํ•ด์•ผ ํ•˜๋Š” ์•ฑ
2. ๊ณต๊ฒฉ์ž๊ฐ€ ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„์„œ scope=read_profile write_data delete_account๋กœ ๋ณ€๊ฒฝ
3. ์‚ฌ์šฉ์ž๊ฐ€ ํ™•์ธํ•˜์ง€ ์•Š๊ณ  ์Šน์ธ
4. ์•ฑ์ด ์˜๋„ํ•˜์ง€ ์•Š์€ ๊ณผ๋„ํ•œ ๊ถŒํ•œ ํš๋“
5. ์•…์˜์  ํ–‰์œ„ ์ˆ˜ํ–‰

๋ฐฉ์–ด ๋ฐฉ๋ฒ•: ๐Ÿ›ก๏ธ

โ€ข ์Šค์ฝ”ํ”„ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ: ํด๋ผ์ด์–ธํŠธ๋ณ„๋กœ ํ—ˆ์šฉ๋œ ์Šค์ฝ”ํ”„ ์‚ฌ์ „ ๋“ฑ๋ก
โ€ข ๋™์˜ ํ™”๋ฉด ๋ช…ํ™•ํ™”: ๊ฐ ์Šค์ฝ”ํ”„๊ฐ€ ์˜๋ฏธํ•˜๋Š” ๋ฐ”๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ช…ํ™•ํžˆ ํ‘œ์‹œ
โ€ข ์ตœ์†Œ ๊ถŒํ•œ ์›์น™: ํ•„์š”ํ•œ ์ตœ์†Œํ•œ์˜ ์Šค์ฝ”ํ”„๋งŒ ์š”์ฒญ
โ€ข ์Šค์ฝ”ํ”„ ๊ฒ€์ฆ: ํ† ํฐ ์‚ฌ์šฉ ์‹œ ์š”์ฒญ๋œ ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์Šค์ฝ”ํ”„ ํ™•์ธ
โ€ข ์„ธ๋ถ„ํ™”๋œ ์Šค์ฝ”ํ”„: ์ฝ๊ธฐ/์“ฐ๊ธฐ/์‚ญ์ œ ๋“ฑ์„ ๋ณ„๋„ ์Šค์ฝ”ํ”„๋กœ ๋ถ„๋ฆฌ
OAuth 2.0 ๋ณด์•ˆ ์œ„ํ˜‘ ๋งต ์ธ๊ฐ€ ์ฝ”๋“œ ๊ฐ€๋กœ์ฑ„๊ธฐ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ์กฐ์ž‘ ํ† ํฐ ์œ ์ถœ CSRF ๊ณต๊ฒฉ ํด๋ผ์ด์–ธํŠธ ์œ„์žฅ ํ† ํฐ ์žฌ์‚ฌ์šฉ ๊ณต๊ฒฉ ์Šค์ฝ”ํ”„ ์กฐ์ž‘ ๋ฐฉ์–ด ์ „๋žต PKCE + HTTPS + ๊ฒ€์ฆ ๊ฐ•ํ™”

๐Ÿ›ก๏ธ OAuth 2.0 ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€

์ง€๊ธˆ๊นŒ์ง€ ๋‹ค์–‘ํ•œ ์œ„ํ˜‘์„ ์‚ดํŽด๋ดค์œผ๋‹ˆ, ์ด์ œ ์ด๋ฅผ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ „์—์„œ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ณด์•ˆ ๊ฐ€์ด๋“œ๋ฅผ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ. ๐Ÿ“‹

๐Ÿ” ์ธ์ฆ ์„œ๋ฒ„ ์ธก ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
โœ… ํด๋ผ์ด์–ธํŠธ ๋“ฑ๋ก ๋ฐ ๊ด€๋ฆฌ โ€ข ํด๋ผ์ด์–ธํŠธ ID์™€ ์‹œํฌ๋ฆฟ์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ƒ์„ฑ (์ถฉ๋ถ„ํ•œ ์—”ํŠธ๋กœํ”ผ)
โ€ข ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI๋ฅผ ์ •ํ™•ํžˆ ๋“ฑ๋กํ•˜๊ณ  ์—„๊ฒฉํ•˜๊ฒŒ ๊ฒ€์ฆ
โ€ข ํด๋ผ์ด์–ธํŠธ๋ณ„ ํ—ˆ์šฉ ์Šค์ฝ”ํ”„ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ด€๋ฆฌ
โ€ข ํด๋ผ์ด์–ธํŠธ ํƒ€์ž…(Public/Confidential) ๊ตฌ๋ถ„ ๋ฐ ์ ์ ˆํ•œ ์ธ์ฆ ๋ฐฉ์‹ ์ ์šฉ

โœ… ํ† ํฐ ๊ด€๋ฆฌ
โ€ข ์•ก์„ธ์Šค ํ† ํฐ ์œ ํšจ๊ธฐ๊ฐ„์„ ์งง๊ฒŒ ์„ค์ • (15๋ถ„~1์‹œ๊ฐ„)
โ€ข ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ํšŒ์ „(Rotation) ๊ตฌํ˜„
โ€ข ํ† ํฐ ์ทจ์†Œ(Revocation) ์—”๋“œํฌ์ธํŠธ ์ œ๊ณต
โ€ข JWT ์‚ฌ์šฉ ์‹œ ์ ์ ˆํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์„ ํƒ (RS256 ๊ถŒ์žฅ, HS256์€ ์‹ ์ค‘ํžˆ)
โ€ข ํ† ํฐ์— ๋ฏผ๊ฐํ•œ ์ •๋ณด ํฌํ•จ ๊ธˆ์ง€

โœ… ๋ณด์•ˆ ๊ธฐ๋Šฅ
โ€ข PKCE ํ•„์ˆ˜ ์ ์šฉ (๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ)
โ€ข State ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ ๊ฐ•์ œ
โ€ข Rate Limiting ๊ตฌํ˜„ (๋ฌด์ฐจ๋ณ„ ๋Œ€์ž… ๊ณต๊ฒฉ ๋ฐฉ์–ด)
โ€ข ์ด์ƒ ํ–‰์œ„ ํƒ์ง€ ๋ฐ ๋กœ๊น…
โ€ข HTTPS ๊ฐ•์ œ (๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์ œ์™ธ)
๐Ÿ” ํด๋ผ์ด์–ธํŠธ ์ธก ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
โœ… ์ธ์ฆ ํ๋ฆ„ โ€ข Authorization Code Grant + PKCE ์‚ฌ์šฉ
โ€ข State ํŒŒ๋ผ๋ฏธํ„ฐ ์ƒ์„ฑ ๋ฐ ๊ฒ€์ฆ
โ€ข ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ํ•˜๋“œ์ฝ”๋”ฉ (๋™์  ์ƒ์„ฑ ๊ธˆ์ง€)
โ€ข ์ธ๊ฐ€ ์ฝ”๋“œ๋Š” ์ฆ‰์‹œ ํ† ํฐ์œผ๋กœ ๊ตํ™˜ (์žฌ์‚ฌ์šฉ ๊ธˆ์ง€)

โœ… ํ† ํฐ ์ €์žฅ ๋ฐ ๊ด€๋ฆฌ
โ€ข ์›น: HttpOnly, Secure, SameSite ์ฟ ํ‚ค ์‚ฌ์šฉ
โ€ข SPA: ๋ฉ”๋ชจ๋ฆฌ ์ €์žฅ ๋˜๋Š” BFF(Backend For Frontend) ํŒจํ„ด
โ€ข ๋ชจ๋ฐ”์ผ: ์‹œ์Šคํ…œ ํ‚ค์ฒด์ธ/ํ‚ค์Šคํ† ์–ด ํ™œ์šฉ
โ€ข LocalStorage/SessionStorage ์‚ฌ์šฉ ์ž์ œ
โ€ข ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ ์ž๋™ ๊ฐฑ์‹  ๋กœ์ง ๊ตฌํ˜„

โœ… API ํ˜ธ์ถœ
โ€ข Authorization ํ—ค๋”๋กœ ํ† ํฐ ์ „์†ก (URL ํŒŒ๋ผ๋ฏธํ„ฐ ๊ธˆ์ง€)
โ€ข ํ•„์š”ํ•œ ์ตœ์†Œํ•œ์˜ ์Šค์ฝ”ํ”„๋งŒ ์š”์ฒญ
โ€ข API ์‘๋‹ต ๊ฒ€์ฆ ๋ฐ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
โ€ข ๋„คํŠธ์›Œํฌ ํƒ€์ž„์•„์›ƒ ์„ค์ •
๐Ÿ” ๋ฆฌ์†Œ์Šค ์„œ๋ฒ„ ์ธก ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
โœ… ํ† ํฐ ๊ฒ€์ฆ โ€ข ๋ชจ๋“  ์š”์ฒญ์—์„œ ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
โ€ข ํ† ํฐ ์„œ๋ช… ๊ฒ€์ฆ (JWT์˜ ๊ฒฝ์šฐ)
โ€ข ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ํ™•์ธ
โ€ข ์Šค์ฝ”ํ”„ ๊ฒ€์ฆ (์š”์ฒญ๋œ ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ๊ถŒํ•œ ํ™•์ธ)
โ€ข ํ† ํฐ ์ทจ์†Œ ๋ชฉ๋ก(Blacklist) ํ™•์ธ

โœ… API ๋ณด์•ˆ
โ€ข HTTPS ํ•„์ˆ˜
โ€ข CORS ์ •์ฑ… ์ ์ ˆํžˆ ์„ค์ •
โ€ข Rate Limiting ์ ์šฉ
โ€ข ์ž…๋ ฅ ๊ฐ’ ๊ฒ€์ฆ ๋ฐ sanitization
โ€ข ์—๋Ÿฌ ๋ฉ”์‹œ์ง€์— ๋ฏผ๊ฐํ•œ ์ •๋ณด ํฌํ•จ ๊ธˆ์ง€

๐Ÿ’ป ์‹ค์ „ ๊ตฌํ˜„ ์˜ˆ์ œ

์ด๋ก ๋งŒ ์•Œ์•„์„œ๋Š” ๋ถ€์กฑํ•˜์ง€. ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ์ฝ”๋“œ๋กœ ์‚ดํŽด๋ณด์ž! ๐Ÿ‘จโ€๐Ÿ’ป
์—ฌ๊ธฐ์„œ๋Š” Node.js/Express๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์ œ๋ฅผ ๋ณด์—ฌ์ค„๊ฒŒ.

๐Ÿ”น ์•ˆ์ „ํ•œ OAuth 2.0 ํด๋ผ์ด์–ธํŠธ ๊ตฌํ˜„
// ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ์„ค์น˜
// npm install express express-session crypto axios

const express = require('express');
const session = require('express-session');
const crypto = require('crypto');
const axios = require('axios');

const app = express();

// ์„ธ์…˜ ์„ค์ •
app.use(session({
    secret: 'your-session-secret',
    resave: false,
    saveUninitialized: false,
    cookie: {
        secure: true, // HTTPS์—์„œ๋งŒ ์ „์†ก
        httpOnly: true, // JavaScript ์ ‘๊ทผ ์ฐจ๋‹จ
        sameSite: 'lax', // CSRF ๋ฐฉ์–ด
        maxAge: 3600000 // 1์‹œ๊ฐ„
    }
}));

// OAuth ์„ค์ •
const OAUTH_CONFIG = {
    clientId: 'your-client-id',
    clientSecret: 'your-client-secret',
    authorizationUrl: 'https://oauth-provider.com/authorize',
    tokenUrl: 'https://oauth-provider.com/token',
    redirectUri: 'https://your-app.com/callback',
    scope: 'read_profile read_email'
};

// PKCE ํ—ฌํผ ํ•จ์ˆ˜
function generateCodeVerifier() {
    return crypto.randomBytes(32).toString('base64url');
}

function generateCodeChallenge(verifier) {
    return crypto
        .createHash('sha256')
        .update(verifier)
        .digest('base64url');
}

// State ์ƒ์„ฑ ํ•จ์ˆ˜
function generateState() {
    return crypto.randomBytes(32).toString('hex');
}

// 1. ๋กœ๊ทธ์ธ ์‹œ์ž‘ - ์ธ๊ฐ€ ์š”์ฒญ
app.get('/login', (req, res) => {
    // PKCE ํŒŒ๋ผ๋ฏธํ„ฐ ์ƒ์„ฑ
    const codeVerifier = generateCodeVerifier();
    const codeChallenge = generateCodeChallenge(codeVerifier);
    
    // State ์ƒ์„ฑ
    const state = generateState();
    
    // ์„ธ์…˜์— ์ €์žฅ (๊ฒ€์ฆ์šฉ)
    req.session.oauthState = state;
    req.session.codeVerifier = codeVerifier;
    
    // ์ธ๊ฐ€ URL ์ƒ์„ฑ
    const params = new URLSearchParams({
        response_type: 'code',
        client_id: OAUTH_CONFIG.clientId,
        redirect_uri: OAUTH_CONFIG.redirectUri,
        scope: OAUTH_CONFIG.scope,
        state: state,
        code_challenge: codeChallenge,
        code_challenge_method: 'S256'
    });
    
    const authUrl = `${OAUTH_CONFIG.authorizationUrl}?${params}`;
    
    // ์‚ฌ์šฉ์ž๋ฅผ ์ธ์ฆ ์„œ๋ฒ„๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
    res.redirect(authUrl);
});

// 2. ์ฝœ๋ฐฑ ์ฒ˜๋ฆฌ - ํ† ํฐ ๊ตํ™˜
app.get('/callback', async (req, res) => {
    try {
        const { code, state } = req.query;
        
        // State ๊ฒ€์ฆ (CSRF ๋ฐฉ์–ด)
        if (!state || state !== req.session.oauthState) {
            throw new Error('Invalid state parameter');
        }
        
        // State ์ผํšŒ์šฉ์œผ๋กœ ๋งŒ๋“ค๊ธฐ
        delete req.session.oauthState;
        
        // ์ธ๊ฐ€ ์ฝ”๋“œ ํ™•์ธ
        if (!code) {
            throw new Error('Authorization code not received');
        }
        
        // ํ† ํฐ ๊ตํ™˜ ์š”์ฒญ
        const tokenResponse = await axios.post(
            OAUTH_CONFIG.tokenUrl,
            new URLSearchParams({
                grant_type: 'authorization_code',
                code: code,
                redirect_uri: OAUTH_CONFIG.redirectUri,
                client_id: OAUTH_CONFIG.clientId,
                client_secret: OAUTH_CONFIG.clientSecret,
                code_verifier: req.session.codeVerifier
            }),
            {
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                }
            }
        );
        
        // Code verifier ์‚ญ์ œ
        delete req.session.codeVerifier;
        
        const { access_token, refresh_token, expires_in } = tokenResponse.data;
        
        // ํ† ํฐ์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅ
        // ์‹ค์ œ ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์•”ํ˜ธํ™”๋œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ
        req.session.accessToken = access_token;
        req.session.refreshToken = refresh_token;
        req.session.tokenExpiry = Date.now() + (expires_in * 1000);
        
        // ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
        const userInfo = await getUserInfo(access_token);
        req.session.user = userInfo;
        
        res.redirect('/dashboard');
        
    } catch (error) {
        console.error('OAuth callback error:', error.message);
        res.status(500).send('Authentication failed');
    }
});

// 3. ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ
async function getUserInfo(accessToken) {
    try {
        const response = await axios.get(
            'https://oauth-provider.com/userinfo',
            {
                headers: {
                    'Authorization': `Bearer ${accessToken}`
                }
            }
        );
        return response.data;
    } catch (error) {
        throw new Error('Failed to fetch user info');
    }
}

// 4. ํ† ํฐ ๊ฐฑ์‹ 
async function refreshAccessToken(refreshToken) {
    try {
        const response = await axios.post(
            OAUTH_CONFIG.tokenUrl,
            new URLSearchParams({
                grant_type: 'refresh_token',
                refresh_token: refreshToken,
                client_id: OAUTH_CONFIG.clientId,
                client_secret: OAUTH_CONFIG.clientSecret
            }),
            {
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                }
            }
        );
        
        return response.data;
    } catch (error) {
        throw new Error('Token refresh failed');
    }
}

// 5. ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด
async function requireAuth(req, res, next) {
    if (!req.session.accessToken) {
        return res.redirect('/login');
    }
    
    // ํ† ํฐ ๋งŒ๋ฃŒ ํ™•์ธ
    if (Date.now() >= req.session.tokenExpiry) {
        try {
            // ํ† ํฐ ๊ฐฑ์‹  ์‹œ๋„
            const newTokens = await refreshAccessToken(req.session.refreshToken);
            req.session.accessToken = newTokens.access_token;
            req.session.tokenExpiry = Date.now() + (newTokens.expires_in * 1000);
            
            if (newTokens.refresh_token) {
                req.session.refreshToken = newTokens.refresh_token;
            }
        } catch (error) {
            // ๊ฐฑ์‹  ์‹คํŒจ ์‹œ ์žฌ๋กœ๊ทธ์ธ ํ•„์š”
            delete req.session.accessToken;
            delete req.session.refreshToken;
            return res.redirect('/login');
        }
    }
    
    next();
}

// 6. ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ
app.get('/dashboard', requireAuth, (req, res) => {
    res.send(`
        <h1>Welcome, ${req.session.user.name}!</h1>
        <p>Email: ${req.session.user.email}</p>
        <a href="/logout">Logout</a>
    `);
});

// 7. ๋กœ๊ทธ์•„์›ƒ
app.get('/logout', async (req, res) => {
    // ํ† ํฐ ์ทจ์†Œ (OAuth ์ œ๊ณต์ž๊ฐ€ ์ง€์›ํ•˜๋Š” ๊ฒฝ์šฐ)
    if (req.session.accessToken) {
        try {
            await axios.post(
                'https://oauth-provider.com/revoke',
                new URLSearchParams({
                    token: req.session.accessToken,
                    client_id: OAUTH_CONFIG.clientId,
                    client_secret: OAUTH_CONFIG.clientSecret
                })
            );
        } catch (error) {
            console.error('Token revocation failed:', error.message);
        }
    }
    
    // ์„ธ์…˜ ์‚ญ์ œ
    req.session.destroy();
    res.redirect('/');
});

app.listen(3000, () => {
    console.log('Server running on https://localhost:3000');
});
๐Ÿ’ก ์ฝ”๋“œ ์„ค๋ช… ์ด ์˜ˆ์ œ๋Š” OAuth 2.0์˜ Authorization Code Grant + PKCE๋ฅผ ๊ตฌํ˜„ํ•œ ๊ฑฐ์•ผ. ์ฃผ์š” ๋ณด์•ˆ ๊ธฐ๋Šฅ:

โ€ข PKCE๋กœ ์ธ๊ฐ€ ์ฝ”๋“œ ๊ฐ€๋กœ์ฑ„๊ธฐ ๋ฐฉ์–ด
โ€ข State ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ CSRF ๊ณต๊ฒฉ ๋ฐฉ์–ด
โ€ข HttpOnly ์ฟ ํ‚ค๋กœ XSS ๊ณต๊ฒฉ ๋ฐฉ์–ด
โ€ข ํ† ํฐ ์ž๋™ ๊ฐฑ์‹  ๊ธฐ๋Šฅ
โ€ข ์•ˆ์ „ํ•œ ํ† ํฐ ์ €์žฅ (์„ธ์…˜ ์‚ฌ์šฉ)
๐Ÿ”น JWT ํ† ํฐ ๊ฒ€์ฆ ๊ตฌํ˜„ (๋ฆฌ์†Œ์Šค ์„œ๋ฒ„)
// JWT ๊ฒ€์ฆ ์˜ˆ์ œ
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

// JWKS ํด๋ผ์ด์–ธํŠธ ์„ค์ • (๊ณต๊ฐœํ‚ค ๊ฐ€์ ธ์˜ค๊ธฐ)
const client = jwksClient({
    jwksUri: 'https://oauth-provider.com/.well-known/jwks.json',
    cache: true,
    cacheMaxAge: 86400000 // 24์‹œ๊ฐ„
});

// ๊ณต๊ฐœํ‚ค ๊ฐ€์ ธ์˜ค๊ธฐ ํ•จ์ˆ˜
function getKey(header, callback) {
    client.getSigningKey(header.kid, (err, key) => {
        if (err) {
            callback(err);
        } else {
            const signingKey = key.publicKey || key.rsaPublicKey;
            callback(null, signingKey);
        }
    });
}

// JWT ๊ฒ€์ฆ ๋ฏธ๋“ค์›จ์–ด
function verifyJWT(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);
    
    // JWT ๊ฒ€์ฆ
    jwt.verify(
        token,
        getKey,
        {
            algorithms: ['RS256'],
            issuer: 'https://oauth-provider.com',
            audience: 'your-api-identifier'
        },
        (err, decoded) => {
            if (err) {
                if (err.name === 'TokenExpiredError') {
                    return res.status(401).json({ error: 'Token expired' });
                }
                return res.status(401).json({ error: 'Invalid token' });
            }
            
            // ์Šค์ฝ”ํ”„ ๊ฒ€์ฆ
            const requiredScope = 'read:data';
            const tokenScopes = decoded.scope ? decoded.scope.split(' ') : [];
            
            if (!tokenScopes.includes(requiredScope)) {
                return res.status(403).json({ error: 'Insufficient permissions' });
            }
            
            // ์ถ”๊ฐ€ ๋ณด์•ˆ ๊ฒ€์ฆ
            // 1. ํ† ํฐ ์ทจ์†Œ ๋ชฉ๋ก ํ™•์ธ
            if (isTokenRevoked(decoded.jti)) {
                return res.status(401).json({ error: 'Token revoked' });
            }
            
            // 2. ํด๋ผ์ด์–ธํŠธ ๊ฒ€์ฆ
            if (!isValidClient(decoded.client_id)) {
                return res.status(401).json({ error: 'Invalid client' });
            }
            
            // ๊ฒ€์ฆ ์„ฑ๊ณต - ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ
            req.user = {
                id: decoded.sub,
                clientId: decoded.client_id,
                scopes: tokenScopes
            };
            
            next();
        }
    );
}

// ํ† ํฐ ์ทจ์†Œ ๋ชฉ๋ก ํ™•์ธ (Redis ๋“ฑ ์‚ฌ์šฉ)
function isTokenRevoked(jti) {
    // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” Redis๋‚˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์กฐํšŒ
    // return redisClient.exists(`revoked:${jti}`);
    return false;
}

// ํด๋ผ์ด์–ธํŠธ ๊ฒ€์ฆ
function isValidClient(clientId) {
    // ๋“ฑ๋ก๋œ ํด๋ผ์ด์–ธํŠธ์ธ์ง€ ํ™•์ธ
    const validClients = ['client-1', 'client-2'];
    return validClients.includes(clientId);
}

// ๋ณดํ˜ธ๋œ API ์—”๋“œํฌ์ธํŠธ
app.get('/api/data', verifyJWT, (req, res) => {
    // ์‚ฌ์šฉ์ž๋ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ
    const data = getUserData(req.user.id);
    res.json(data);
});

// ์Šค์ฝ”ํ”„๋ณ„ ๊ถŒํ•œ ๊ฒ€์ฆ ๋ฏธ๋“ค์›จ์–ด
function requireScope(scope) {
    return (req, res, next) => {
        if (!req.user || !req.user.scopes.includes(scope)) {
            return res.status(403).json({ error: 'Insufficient permissions' });
        }
        next();
    };
}

// ์“ฐ๊ธฐ ๊ถŒํ•œ์ด ํ•„์š”ํ•œ ์—”๋“œํฌ์ธํŠธ
app.post('/api/data', verifyJWT, requireScope('write:data'), (req, res) => {
    // ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
    const result = createData(req.user.id, req.body);
    res.json(result);
});

๐Ÿ” OAuth 2.0 vs OpenID Connect

๋งŽ์€ ์‚ฌ๋žŒ๋“ค์ด OAuth 2.0๊ณผ OpenID Connect(OIDC)๋ฅผ ํ—ท๊ฐˆ๋ ค ํ•ด. ๐Ÿคท
๊ฐ„๋‹จํžˆ ๋งํ•˜๋ฉด, OAuth 2.0์€ ์ธ๊ฐ€(Authorization) ํ”„๋ ˆ์ž„์›Œํฌ์ด๊ณ , OpenID Connect๋Š” ์ธ์ฆ(Authentication) ํ”„๋กœํ† ์ฝœ์ด์•ผ.

๐Ÿ”‘ ํ•ต์‹ฌ ์ฐจ์ด์  OAuth 2.0:
โ€ข "์ด ์•ฑ์ด ๋‚ด ์‚ฌ์ง„์— ์ ‘๊ทผํ•ด๋„ ๋ผ?" (๊ถŒํ•œ ๋ถ€์—ฌ)
โ€ข ์•ก์„ธ์Šค ํ† ํฐ ๋ฐœ๊ธ‰
โ€ข ๋ฆฌ์†Œ์Šค ์ ‘๊ทผ ๊ถŒํ•œ ๊ด€๋ฆฌ
โ€ข ์‚ฌ์šฉ์ž ์‹ ์› ํ™•์ธ์€ ๋ถ€์ฐจ์ 

OpenID Connect:
โ€ข "๋„ˆ ๋ˆ„๊ตฌ์•ผ?" (์‹ ์› ํ™•์ธ)
โ€ข ID ํ† ํฐ(JWT) ๋ฐœ๊ธ‰
โ€ข ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ํ”„๋กœํ•„ ์ •๋ณด ์ œ๊ณต
โ€ข OAuth 2.0 ์œ„์— ๊ตฌ์ถ•๋œ ๋ ˆ์ด์–ด

OpenID Connect๋Š” OAuth 2.0์„ ํ™•์žฅํ•ด์„œ ๋งŒ๋“ค์–ด์กŒ์–ด. ๐Ÿ—๏ธ
OAuth 2.0์˜ ์ธ๊ฐ€ ํ๋ฆ„์— openid ์Šค์ฝ”ํ”„๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ID ํ† ํฐ์ด๋ผ๋Š” ์ƒˆ๋กœ์šด ํ† ํฐ ํƒ€์ž…์„ ๋„์ž…ํ–ˆ์ง€.

// OpenID Connect ์ธ๊ฐ€ ์š”์ฒญ
https://authorization-server.com/auth?
  response_type=code&
  client_id=YOUR_CLIENT_ID&
  redirect_uri=https://your-app.com/callback&
  scope=openid profile email&  // openid ์Šค์ฝ”ํ”„ ํ•„์ˆ˜
  state=random_string&
  nonce=random_nonce  // OIDC ์ถ”๊ฐ€ ํŒŒ๋ผ๋ฏธํ„ฐ

// ํ† ํฐ ์‘๋‹ต (OIDC)
{
  "access_token": "eyJhbGc...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "tGzv3JOk...",
  "id_token": "eyJhbGciOiJSUzI1NiIs..."  // ID ํ† ํฐ ์ถ”๊ฐ€
}

// ID ํ† ํฐ ๋””์ฝ”๋”ฉ ๊ฒฐ๊ณผ
{
  "iss": "https://oauth-provider.com",
  "sub": "user-12345",
  "aud": "your-client-id",
  "exp": 1234567890,
  "iat": 1234564290,
  "nonce": "random_nonce",
  "name": "ํ™๊ธธ๋™",
  "email": "hong@example.com",
  "picture": "https://example.com/photo.jpg"
}
๐Ÿ’ก ์–ธ์ œ ๋ฌด์—‡์„ ์‚ฌ์šฉํ• ๊นŒ? OAuth 2.0๋งŒ ์‚ฌ์šฉ: API ์ ‘๊ทผ ๊ถŒํ•œ๋งŒ ํ•„์š”ํ•œ ๊ฒฝ์šฐ (์˜ˆ: ํŠธ์œ„ํ„ฐ ๋ด‡์ด ํŠธ์œ— ์ž‘์„ฑ)
OpenID Connect ์‚ฌ์šฉ: ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ๋ฐ ์‹ ์› ํ™•์ธ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ (์˜ˆ: "๊ตฌ๊ธ€๋กœ ๋กœ๊ทธ์ธ")
๋‘˜ ๋‹ค ์‚ฌ์šฉ: ๋กœ๊ทธ์ธ + API ์ ‘๊ทผ์ด ๋ชจ๋‘ ํ•„์š”ํ•œ ๊ฒฝ์šฐ (๋Œ€๋ถ€๋ถ„์˜ ํ˜„๋Œ€ ์•ฑ)

๐ŸŒ ์‹ค์ œ ์„œ๋น„์Šค์˜ OAuth 2.0 ๊ตฌํ˜„ ์‚ฌ๋ก€

์ด๋ก ๊ณผ ์˜ˆ์ œ๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•ด. ์‹ค์ œ ๋Œ€ํ˜• ์„œ๋น„์Šค๋“ค์ด ์–ด๋–ป๊ฒŒ OAuth 2.0์„ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ๋Š”์ง€ ์‚ดํŽด๋ณด์ž! ๐Ÿข

๐Ÿ”น Google OAuth 2.0

๊ตฌ๊ธ€์€ ๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋Š” OAuth ์ œ๊ณต์ž ์ค‘ ํ•˜๋‚˜์•ผ. ๐Ÿ”
OpenID Connect๋„ ์™„๋ฒฝํ•˜๊ฒŒ ์ง€์›ํ•˜๋ฉฐ, ๋งค์šฐ ์ƒ์„ธํ•œ ๋ฌธ์„œ์™€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ œ๊ณตํ•ด.

ํŠน์ง•: โ€ข ๋‹ค์–‘ํ•œ ์Šค์ฝ”ํ”„ ์ œ๊ณต (Gmail, Drive, Calendar, YouTube ๋“ฑ)
โ€ข ์„ธ๋ถ„ํ™”๋œ ๊ถŒํ•œ ๊ด€๋ฆฌ
โ€ข OAuth 2.0 Playground ์ œ๊ณต (ํ…Œ์ŠคํŠธ ๋„๊ตฌ)
โ€ข ์ž๋™ ํ† ํฐ ๊ฐฑ์‹  ์ง€์›
โ€ข ๊ฐ•๋ ฅํ•œ ๋ณด์•ˆ ์ •์ฑ… (์˜์‹ฌ์Šค๋Ÿฌ์šด ํ™œ๋™ ์ž๋™ ๊ฐ์ง€)

๋ณด์•ˆ ๊ธฐ๋Šฅ:
โ€ข PKCE ํ•„์ˆ˜ (๋ชจ๋ฐ”์ผ ์•ฑ)
โ€ข ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ์—„๊ฒฉํ•œ ๊ฒ€์ฆ
โ€ข ์•ฑ ๊ฒ€์ฆ ํ”„๋กœ์„ธ์Šค (OAuth consent screen)
โ€ข ๋น„์ •์ƒ ํ™œ๋™ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์•Œ๋ฆผ
๐Ÿ”น GitHub OAuth

๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ์นœ์ˆ™ํ•œ GitHub์˜ OAuth ๊ตฌํ˜„์€ ์‹ฌํ”Œํ•˜๋ฉด์„œ๋„ ๊ฐ•๋ ฅํ•ด. ๐Ÿ’ป

ํŠน์ง•: โ€ข ๊ฐœ๋ฐœ์ž ์นœํ™”์ ์ธ API
โ€ข ์„ธ๋ถ„ํ™”๋œ ๊ถŒํ•œ (repo, user, gist ๋“ฑ)
โ€ข Personal Access Token ์ง€์›
โ€ข OAuth Apps์™€ GitHub Apps ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹ ์ œ๊ณต

๋…ํŠนํ•œ ์ :
โ€ข Device Flow ์ง€์› (CLI ๋„๊ตฌ์šฉ)
โ€ข Fine-grained personal access tokens (๋ฒ ํƒ€)
โ€ข ์กฐ์ง ์ˆ˜์ค€์˜ OAuth ์ •์ฑ… ์„ค์ • ๊ฐ€๋Šฅ
๐Ÿ”น ์นด์นด์˜ค ๋กœ๊ทธ์ธ

ํ•œ๊ตญ์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ์†Œ์…œ ๋กœ๊ทธ์ธ์ด์ง€. ๐Ÿ’ฌ

ํŠน์ง•: โ€ข ๊ฐ„ํŽธํ•œ ํ†ตํ•ฉ (JavaScript SDK ์ œ๊ณต)
โ€ข ์นด์นด์˜คํ†ก ๋ฉ”์‹œ์ง€ ์ „์†ก ๊ถŒํ•œ
โ€ข ์นด์นด์˜คํŽ˜์ด ์—ฐ๋™
โ€ข ์นœ๊ตฌ ๋ชฉ๋ก ์ ‘๊ทผ (๋™์˜ ํ•„์š”)

๋ณด์•ˆ ๊ธฐ๋Šฅ:
โ€ข ์•ฑ ๋“ฑ๋ก ์‹œ ์‹ฌ์‚ฌ ๊ณผ์ •
โ€ข ๋น„์ฆˆ๋‹ˆ์Šค ์ธ์ฆ ํ•„์ˆ˜ (ํŠน์ • ๊ถŒํ•œ)
โ€ข ์‚ฌ์šฉ์ž ๋™์˜ ์ฒ ํšŒ ๊ธฐ๋Šฅ
โ€ข ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ ๊ฐ•ํ™” (์ตœ์†Œ ๊ถŒํ•œ ์›์น™)
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ ๊ฐ OAuth ์ œ๊ณต์ž๋งˆ๋‹ค ๊ตฌํ˜„ ์„ธ๋ถ€์‚ฌํ•ญ์ด ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์–ด. ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ๋ฐ˜๋“œ์‹œ ์ฐธ๊ณ ํ•˜๊ณ , ์ œ๊ณต์ž๋ณ„ ํŠน์ˆ˜ํ•œ ์š”๊ตฌ์‚ฌํ•ญ์„ ํ™•์ธํ•ด์•ผ ํ•ด.

๐Ÿงช OAuth 2.0 ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•

๊ตฌํ˜„์ด ๋๋‚ฌ๋‹ค๊ณ  ์•ˆ์‹ฌํ•˜๋ฉด ์•ˆ ๋ผ. ๐Ÿ”ฌ
์‹ค์ œ๋กœ ๋ณด์•ˆ์ด ์ œ๋Œ€๋กœ ์ ์šฉ๋˜์—ˆ๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์ด ๋งค์šฐ ์ค‘์š”ํ•ด.

๐Ÿ”น ์ž๋™ํ™”๋œ ๋ณด์•ˆ ์Šค์บ” ๋„๊ตฌ
1. OWASP ZAP (Zed Attack Proxy) โ€ข ๋ฌด๋ฃŒ ์˜คํ”ˆ์†Œ์Šค ๋ณด์•ˆ ์Šค์บ๋„ˆ
โ€ข OAuth 2.0 ์ทจ์•ฝ์  ์ž๋™ ํƒ์ง€
โ€ข MITM(Man-in-the-Middle) ํ”„๋ก์‹œ ๊ธฐ๋Šฅ
โ€ข API ํ…Œ์ŠคํŠธ ์ง€์›

2. Burp Suite
โ€ข ์ „๋ฌธ๊ฐ€์šฉ ์›น ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ๋„๊ตฌ
โ€ข OAuth 2.0 ํ”Œ๋Ÿฌ๊ทธ์ธ ์ œ๊ณต
โ€ข ์š”์ฒญ/์‘๋‹ต ์กฐ์ž‘ ๋ฐ ๋ถ„์„
โ€ข ์ž๋™ํ™”๋œ ์Šค์บ” ๊ธฐ๋Šฅ

3. OAuth 2.0 Security Testing Tool
โ€ข OAuth ์ „์šฉ ํ…Œ์ŠคํŠธ ๋„๊ตฌ
โ€ข ๋‹ค์–‘ํ•œ ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค ์‹œ๋ฎฌ๋ ˆ์ด์…˜
โ€ข ํ† ํฐ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ
๐Ÿ”น ์ˆ˜๋™ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
๐Ÿ”ด ํ•„์ˆ˜ ํ…Œ์ŠคํŠธ ํ•ญ๋ชฉ 1. ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ
โœ“ ๋“ฑ๋ก๋˜์ง€ ์•Š์€ URI๋กœ ๋ณ€๊ฒฝ ์‹œ๋„
โœ“ Path traversal ์‹œ๋„ (../ ๋“ฑ)
โœ“ ์„œ๋ธŒ๋„๋ฉ”์ธ ๋ณ€๊ฒฝ ์‹œ๋„
โœ“ ์˜คํ”ˆ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์ทจ์•ฝ์  ํ™•์ธ

2. State ํŒŒ๋ผ๋ฏธํ„ฐ ํ…Œ์ŠคํŠธ
โœ“ State ์—†์ด ์š”์ฒญ
โœ“ ์ž˜๋ชป๋œ State ๊ฐ’ ์‚ฌ์šฉ
โœ“ State ์žฌ์‚ฌ์šฉ ์‹œ๋„
โœ“ State ์˜ˆ์ธก ๊ฐ€๋Šฅ์„ฑ ํ™•์ธ

3. PKCE ํ…Œ์ŠคํŠธ
โœ“ Code verifier ์—†์ด ํ† ํฐ ๊ตํ™˜ ์‹œ๋„
โœ“ ์ž˜๋ชป๋œ Code verifier ์‚ฌ์šฉ
โœ“ Code challenge ์กฐ์ž‘ ์‹œ๋„

4. ํ† ํฐ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ
โœ“ ๋งŒ๋ฃŒ๋œ ํ† ํฐ ์‚ฌ์šฉ ์‹œ๋„
โœ“ ํ† ํฐ ์žฌ์‚ฌ์šฉ ํ…Œ์ŠคํŠธ
โœ“ ํ† ํฐ ์กฐ์ž‘ ์‹œ๋„ (JWT ์„œ๋ช… ๋ณ€๊ฒฝ)
โœ“ ํ† ํฐ ์œ ์ถœ ๊ฒฝ๋กœ ํ™•์ธ (๋กœ๊ทธ, ํžˆ์Šคํ† ๋ฆฌ ๋“ฑ)

5. ์Šค์ฝ”ํ”„ ํ…Œ์ŠคํŠธ
โœ“ ๊ถŒํ•œ ์—†๋Š” ๋ฆฌ์†Œ์Šค ์ ‘๊ทผ ์‹œ๋„
โœ“ ์Šค์ฝ”ํ”„ ์ƒ์Šน ๊ณต๊ฒฉ ์‹œ๋„
โœ“ ๊ณผ๋„ํ•œ ๊ถŒํ•œ ์š”์ฒญ ํ…Œ์ŠคํŠธ

6. ์ธ๊ฐ€ ์ฝ”๋“œ ํ…Œ์ŠคํŠธ
โœ“ ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ ์‹œ๋„
โœ“ ์ฝ”๋“œ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ํ™•์ธ
โœ“ ๋‹ค๋ฅธ ํด๋ผ์ด์–ธํŠธ๋กœ ์ฝ”๋“œ ์‚ฌ์šฉ ์‹œ๋„
// ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ ์˜ˆ์‹œ (Python)
import requests
import time

# 1. ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ์กฐ์ž‘ ํ…Œ์ŠคํŠธ
def test_redirect_uri_manipulation():
    malicious_uris = [
        'https://attacker.com/steal',
        'https://legitimate-app.com/../attacker',
        'https://legitimate-app.com?redirect=https://attacker.com',
        'http://legitimate-app.com/callback'  # HTTP ์‹œ๋„
    ]
    
    for uri in malicious_uris:
        params = {
            'response_type': 'code',
            'client_id': 'test-client',
            'redirect_uri': uri,
            'scope': 'read_profile',
            'state': 'random_state'
        }
        
        response = requests.get(
            'https://auth-server.com/authorize',
            params=params,
            allow_redirects=False
        )
        
        # ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๊ฐ€ ํ—ˆ์šฉ๋˜๋ฉด ์ทจ์•ฝ์ 
        if response.status_code == 302:
            print(f"โš ๏ธ  ์ทจ์•ฝ์  ๋ฐœ๊ฒฌ: {uri}")
        else:
            print(f"โœ… ์•ˆ์ „: {uri}")

# 2. State ํŒŒ๋ผ๋ฏธํ„ฐ ํ…Œ์ŠคํŠธ
def test_state_parameter():
    # State ์—†์ด ์š”์ฒญ
    params_no_state = {
        'response_type': 'code',
        'client_id': 'test-client',
        'redirect_uri': 'https://app.com/callback',
        'scope': 'read_profile'
    }
    
    response = requests.get(
        'https://auth-server.com/authorize',
        params=params_no_state
    )
    
    if 'error' not in response.url:
        print("โš ๏ธ  State ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ ์—†์Œ")
    else:
        print("โœ… State ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ ์ •์ƒ")

# 3. ํ† ํฐ ๋งŒ๋ฃŒ ํ…Œ์ŠคํŠธ
def test_token_expiration():
    # ํ† ํฐ ๋ฐœ๊ธ‰
    token = get_access_token()
    
    # ์ฆ‰์‹œ ์‚ฌ์šฉ (์ •์ƒ)
    response1 = requests.get(
        'https://api.example.com/data',
        headers={'Authorization': f'Bearer {token}'}
    )
    print(f"์ฆ‰์‹œ ์‚ฌ์šฉ: {response1.status_code}")
    
    # ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ๋Œ€๊ธฐ
    time.sleep(3700)  # 1์‹œ๊ฐ„ ํ›„
    
    # ๋งŒ๋ฃŒ ํ›„ ์‚ฌ์šฉ ์‹œ๋„
    response2 = requests.get(
        'https://api.example.com/data',
        headers={'Authorization': f'Bearer {token}'}
    )
    
    if response2.status_code == 401:
        print("โœ… ํ† ํฐ ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ ์ •์ƒ")
    else:
        print("โš ๏ธ  ๋งŒ๋ฃŒ๋œ ํ† ํฐ์ด ์—ฌ์ „ํžˆ ์œ ํšจํ•จ")

# 4. ์Šค์ฝ”ํ”„ ๊ถŒํ•œ ํ…Œ์ŠคํŠธ
def test_scope_enforcement():
    # read ๊ถŒํ•œ๋งŒ ์žˆ๋Š” ํ† ํฐ
    read_token = get_token_with_scope('read_profile')
    
    # write ์ž‘์—… ์‹œ๋„
    response = requests.post(
        'https://api.example.com/profile',
        headers={'Authorization': f'Bearer {read_token}'},
        json={'name': 'New Name'}
    )
    
    if response.status_code == 403:
        print("โœ… ์Šค์ฝ”ํ”„ ๊ฒ€์ฆ ์ •์ƒ")
    else:
        print("โš ๏ธ  ์Šค์ฝ”ํ”„ ๊ฒ€์ฆ ์‹คํŒจ")

# ํ…Œ์ŠคํŠธ ์‹คํ–‰
if __name__ == '__main__':
    print("OAuth 2.0 ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ์‹œ์ž‘...\n")
    test_redirect_uri_manipulation()
    test_state_parameter()
    test_token_expiration()
    test_scope_enforcement()

๐Ÿ“Š OAuth 2.0 ๋ณด์•ˆ ์‚ฌ๊ณ  ์‚ฌ๋ก€ ๋ถ„์„

์‹ค์ œ๋กœ ๋ฐœ์ƒํ•œ ๋ณด์•ˆ ์‚ฌ๊ณ ๋ฅผ ํ†ตํ•ด ๋ฐฐ์šฐ๋Š” ๊ฒŒ ๊ฐ€์žฅ ํšจ๊ณผ์ ์ด์•ผ. ๐Ÿ“š
๋ช‡ ๊ฐ€์ง€ ์œ ๋ช…ํ•œ ์‚ฌ๋ก€๋ฅผ ์‚ดํŽด๋ณด์ž.

๐Ÿ”น ์‚ฌ๋ก€ 1: ํŽ˜์ด์Šค๋ถ OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ์ทจ์•ฝ์  (2014)
๐Ÿ”ด ์‚ฌ๊ณ  ๊ฐœ์š” ํŽ˜์ด์Šค๋ถ์˜ OAuth ๊ตฌํ˜„์—์„œ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ๊ฒ€์ฆ์ด ๋ถˆ์™„์ „ํ–ˆ์–ด. ๊ณต๊ฒฉ์ž๊ฐ€ ํŠน์ • ๋„๋ฉ”์ธ์˜ ์„œ๋ธŒ๋„๋ฉ”์ธ์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•  ์ˆ˜ ์žˆ์—ˆ๊ณ , ์ด๋ฅผ ํ†ตํ•ด ์•ก์„ธ์Šค ํ† ํฐ์„ ํƒˆ์ทจํ•  ์ˆ˜ ์žˆ์—ˆ์ง€.

๊ณต๊ฒฉ ๋ฐฉ๋ฒ•:
1. ๊ณต๊ฒฉ์ž๊ฐ€ ์ทจ์•ฝํ•œ ์„œ๋ธŒ๋„๋ฉ”์ธ ๋ฐœ๊ฒฌ (์˜ˆ: vulnerable.facebook.com)
2. ํ•ด๋‹น ์„œ๋ธŒ๋„๋ฉ”์ธ์„ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI๋กœ ์„ค์ •
3. ์‚ฌ์šฉ์ž๋ฅผ ์•…์„ฑ ๋งํฌ๋กœ ์œ ๋„
4. ์•ก์„ธ์Šค ํ† ํฐ์ด ๊ณต๊ฒฉ์ž์˜ ์„œ๋ฒ„๋กœ ์ „์†ก๋จ

์˜ํ–ฅ:
์ˆ˜๋ฐฑ๋งŒ ๋ช…์˜ ์‚ฌ์šฉ์ž ๊ณ„์ •์ด ์œ„ํ—˜์— ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ์—ˆ์Œ

๊ตํ›ˆ:
โ€ข ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI๋Š” ์ •ํ™•ํžˆ ๋งค์นญํ•ด์•ผ ํ•จ (์™€์ผ๋“œ์นด๋“œ ๊ธˆ์ง€)
โ€ข ์„œ๋ธŒ๋„๋ฉ”์ธ๋„ ๋ณ„๋„๋กœ ๊ฒ€์ฆ ํ•„์š”
โ€ข ์ •๊ธฐ์ ์ธ ๋ณด์•ˆ ๊ฐ์‚ฌ ํ•„์ˆ˜
๐Ÿ”น ์‚ฌ๋ก€ 2: ๋ชจ๋ฐ”์ผ ์•ฑ OAuth ์ธ๊ฐ€ ์ฝ”๋“œ ๊ฐ€๋กœ์ฑ„๊ธฐ (2016)
๐Ÿ”ด ์‚ฌ๊ณ  ๊ฐœ์š” ์—ฌ๋Ÿฌ ๋ชจ๋ฐ”์ผ ์•ฑ์—์„œ ์ปค์Šคํ…€ URL ์Šคํ‚ด์„ ์‚ฌ์šฉํ•œ OAuth ๊ตฌํ˜„์ด ์ทจ์•ฝํ–ˆ์–ด. ์•…์„ฑ ์•ฑ์ด ๋™์ผํ•œ URL ์Šคํ‚ด์„ ๋“ฑ๋กํ•ด์„œ ์ธ๊ฐ€ ์ฝ”๋“œ๋ฅผ ๊ฐ€๋กœ์ฑŒ ์ˆ˜ ์žˆ์—ˆ์ง€.

๊ณต๊ฒฉ ๋ฐฉ๋ฒ•:
1. ์ •์ƒ ์•ฑ์ด myapp://callback ์Šคํ‚ด ์‚ฌ์šฉ
2. ์•…์„ฑ ์•ฑ๋„ ๋™์ผํ•œ ์Šคํ‚ด ๋“ฑ๋ก
3. ์‚ฌ์šฉ์ž๊ฐ€ OAuth ์ธ์ฆ ์™„๋ฃŒ
4. OS๊ฐ€ ์•…์„ฑ ์•ฑ์œผ๋กœ ์ธ๊ฐ€ ์ฝ”๋“œ ์ „๋‹ฌ (๋จผ์ € ์„ค์น˜๋œ ์•ฑ ๋˜๋Š” ๋žœ๋ค)
5. ์•…์„ฑ ์•ฑ์ด ์ธ๊ฐ€ ์ฝ”๋“œ๋กœ ํ† ํฐ ๋ฐœ๊ธ‰๋ฐ›์Œ

์˜ํ–ฅ:
์ˆ˜์‹ญ ๊ฐœ์˜ ์ธ๊ธฐ ์•ฑ์ด ์˜ํ–ฅ๋ฐ›์Œ

๊ตํ›ˆ:
โ€ข PKCE ํ•„์ˆ˜ ์‚ฌ์šฉ (์ด ๊ณต๊ฒฉ ์™„๋ฒฝํžˆ ๋ฐฉ์–ด)
โ€ข App Links (Android) / Universal Links (iOS) ์‚ฌ์šฉ
โ€ข ์ปค์Šคํ…€ URL ์Šคํ‚ด ์‚ฌ์šฉ ์ž์ œ
๐Ÿ”น ์‚ฌ๋ก€ 3: ํ† ํฐ ๋กœ๊ทธ ๋…ธ์ถœ ์‚ฌ๊ณ  (2018)
๐Ÿ”ด ์‚ฌ๊ณ  ๊ฐœ์š” ํ•œ ๋Œ€ํ˜• ์†Œ์…œ ๋ฏธ๋””์–ด ํ”Œ๋žซํผ์—์„œ ์•ก์„ธ์Šค ํ† ํฐ์ด ์„œ๋ฒ„ ๋กœ๊ทธ์— ํ‰๋ฌธ์œผ๋กœ ๊ธฐ๋ก๋˜๋Š” ๋ฒ„๊ทธ๊ฐ€ ๋ฐœ๊ฒฌ๋˜์—ˆ์–ด. ๋‚ด๋ถ€ ์ง์›๋“ค์ด ์ด ๋กœ๊ทธ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์—ˆ๊ณ , ์ˆ˜๋ฐฑ๋งŒ ๋ช…์˜ ์‚ฌ์šฉ์ž ํ† ํฐ์ด ๋…ธ์ถœ๋˜์—ˆ์ง€.

์›์ธ:
โ€ข ๋””๋ฒ„๊น… ๋ชฉ์ ์œผ๋กœ ์ถ”๊ฐ€ํ•œ ๋กœ๊น… ์ฝ”๋“œ๋ฅผ ํ”„๋กœ๋•์…˜์—์„œ ์ œ๊ฑฐํ•˜์ง€ ์•Š์Œ
โ€ข ๋กœ๊ทธ ํ•„ํ„ฐ๋ง ๋ฏธํก
โ€ข ๋‚ด๋ถ€ ์ ‘๊ทผ ํ†ต์ œ ๋ถ€์กฑ

์˜ํ–ฅ:
์ˆ˜๋ฐฑ๋งŒ ๋ช…์˜ ๊ณ„์ •์ด ์ž ์žฌ์ ์œผ๋กœ ์œ„ํ—˜์— ๋…ธ์ถœ

๊ตํ›ˆ:
โ€ข ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์ ˆ๋Œ€ ๋กœ๊ทธ์— ๊ธฐ๋กํ•˜์ง€ ๋ง ๊ฒƒ
โ€ข ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์ „ ์ฒ ์ €ํ•œ ์ฝ”๋“œ ๋ฆฌ๋ทฐ
โ€ข ๋กœ๊ทธ ์ ‘๊ทผ ๊ถŒํ•œ ์ตœ์†Œํ™”
โ€ข ์ž๋™ํ™”๋œ ๋ฏผ๊ฐ์ •๋ณด ํƒ์ง€ ๋„๊ตฌ ์‚ฌ์šฉ
โœจ ๊ณตํ†ต ๊ตํ›ˆ 1. ๋ณด์•ˆ์€ ํ•œ ๋ฒˆ์— ๋๋‚˜์ง€ ์•Š์•„: ์ง€์†์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ์—…๋ฐ์ดํŠธ ํ•„์š”
2. ์ตœ์‹  ๋ณด์•ˆ ํ‘œ์ค€ ์ ์šฉ: PKCE, HTTPS ๋“ฑ์€ ํ•„์ˆ˜
3. ์ตœ์†Œ ๊ถŒํ•œ ์›์น™: ํ•„์š”ํ•œ ์ตœ์†Œํ•œ์˜ ๊ถŒํ•œ๋งŒ ์š”์ฒญ
4. ๋‹ค์ธต ๋ฐฉ์–ด: ํ•˜๋‚˜์˜ ๋ณด์•ˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜์—๋งŒ ์˜์กดํ•˜์ง€ ๋ง ๊ฒƒ
5. ์ •๊ธฐ์ ์ธ ๋ณด์•ˆ ๊ฐ์‚ฌ: ์™ธ๋ถ€ ์ „๋ฌธ๊ฐ€์˜ ๊ฒ€ํ†  ํ•„์š”

๐Ÿš€ OAuth 2.0์˜ ๋ฏธ๋ž˜: OAuth 2.1

OAuth 2.0์ด ์™„๋ฒฝํ•œ ๊ฑด ์•„๋‹ˆ์•ผ. ๊ทธ๋ž˜์„œ OAuth 2.1์ด ๊ฐœ๋ฐœ ์ค‘์ด์•ผ! ๐Ÿ”ฎ
2020๋…„๋ถ€ํ„ฐ ์ž‘์—…์ด ์‹œ์ž‘๋˜์—ˆ๊ณ , ํ˜„์žฌ ๋“œ๋ž˜ํ”„ํŠธ ๋‹จ๊ณ„์— ์žˆ์–ด.

๐Ÿ†• OAuth 2.1์˜ ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ 1. PKCE ํ•„์ˆ˜ํ™”
โ€ข ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์— PKCE ์ ์šฉ (Public, Confidential ๋ชจ๋‘)
โ€ข Implicit Grant ๊ณต์‹ ํ๊ธฐ

2. ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ์ •ํ™•ํ•œ ๋งค์นญ
โ€ข ์™€์ผ๋“œ์นด๋“œ ๊ธˆ์ง€
โ€ข ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๊นŒ์ง€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•ด์•ผ ํ•จ

3. ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๋ณด์•ˆ ๊ฐ•ํ™”
โ€ข ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ํšŒ์ „ ๊ถŒ์žฅ
โ€ข ์ผํšŒ์šฉ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ

4. ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ ํ†ตํ•ฉ
โ€ข RFC 8252 (๋ชจ๋ฐ”์ผ ์•ฑ OAuth)
โ€ข RFC 8628 (Device Flow)
โ€ข RFC 8707 (Resource Indicators)

5. ๋‹จ์ˆœํ™”
โ€ข ๋ถˆํ•„์š”ํ•œ ์˜ต์…˜ ์ œ๊ฑฐ
โ€ข ๋ช…ํ™•ํ•œ ๊ฐ€์ด๋“œ๋ผ์ธ ์ œ๊ณต

OAuth 2.1์€ ๊ธฐ์กด OAuth 2.0์˜ ๋ณด์•ˆ ์ทจ์•ฝ์ ์„ ํ•ด๊ฒฐํ•˜๊ณ  ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ํ‘œ์ค€ํ™”ํ•˜๋Š” ๊ฒŒ ๋ชฉํ‘œ์•ผ. ๐ŸŽฏ
ํ•˜์œ„ ํ˜ธํ™˜์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ๋” ์•ˆ์ „ํ•œ ๊ตฌํ˜„์„ ๊ฐ•์ œํ•˜๋Š” ๊ฑฐ์ง€.

๐Ÿ’ก ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ ์กฐ์–ธ OAuth 2.1์ด ์•„์ง ์ •์‹ ํ‘œ์ค€์€ ์•„๋‹ˆ์ง€๋งŒ, ์ง€๊ธˆ๋ถ€ํ„ฐ OAuth 2.1์˜ ๊ถŒ์žฅ์‚ฌํ•ญ์„ ๋”ฐ๋ฅด๋Š” ๊ฒŒ ์ข‹์•„. ํŠนํžˆ PKCE ์‚ฌ์šฉ๊ณผ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ์ •ํ™•ํ•œ ๋งค์นญ์€ ์ง€๊ธˆ ๋‹น์žฅ ์ ์šฉํ•  ์ˆ˜ ์žˆ๊ณ , ๋ณด์•ˆ์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์–ด.

๐Ÿ’ผ ์‹ค๋ฌด์—์„œ์˜ OAuth 2.0 ๊ตฌํ˜„ ํŒ

์ด๋ก ๊ณผ ์‹ค์ „์€ ๋‹ค๋ฅด์ง€. ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ OAuth 2.0์„ ๊ตฌํ˜„ํ•  ๋•Œ ์œ ์šฉํ•œ ํŒ๋“ค์„ ๊ณต์œ ํ• ๊ฒŒ! ๐Ÿ’ก

๐Ÿ”น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ ํƒ

์ฒ˜์Œ๋ถ€ํ„ฐ ์ง์ ‘ ๊ตฌํ˜„ํ•˜์ง€ ๋ง๊ณ , ๊ฒ€์ฆ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ๐Ÿ“š

์ถ”์ฒœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ: Node.js:
โ€ข Passport.js (๋‹ค์–‘ํ•œ OAuth ์ œ๊ณต์ž ์ง€์›)
โ€ข simple-oauth2 (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„)
โ€ข openid-client (OpenID Connect ์ง€์›)

Python:
โ€ข Authlib (ํฌ๊ด„์ ์ธ OAuth/OIDC ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)
โ€ข requests-oauthlib (๊ฐ„๋‹จํ•œ ํด๋ผ์ด์–ธํŠธ)
โ€ข python-social-auth (Django/Flask ํ†ตํ•ฉ)

Java:
โ€ข Spring Security OAuth
โ€ข Nimbus OAuth 2.0 SDK
โ€ข Pac4j

JavaScript (ํ”„๋ก ํŠธ์—”๋“œ):
โ€ข oidc-client-js (SPA์šฉ)
โ€ข hello.js (๋‹ค์ค‘ ์ œ๊ณต์ž ์ง€์›)
โ€ข Auth0 SDK
๐Ÿ”น ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ๊ด€๋ฆฌ
// ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ OAuth ์„ค์ • ๊ด€๋ฆฌ (.env ํŒŒ์ผ)
# ๊ฐœ๋ฐœ ํ™˜๊ฒฝ
OAUTH_CLIENT_ID=dev-client-id
OAUTH_CLIENT_SECRET=dev-client-secret
OAUTH_REDIRECT_URI=http://localhost:3000/callback
OAUTH_AUTHORIZATION_URL=https://oauth-provider.com/authorize
OAUTH_TOKEN_URL=https://oauth-provider.com/token

# ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ
OAUTH_CLIENT_ID=prod-client-id
OAUTH_CLIENT_SECRET=prod-client-secret
OAUTH_REDIRECT_URI=https://myapp.com/callback
OAUTH_AUTHORIZATION_URL=https://oauth-provider.com/authorize
OAUTH_TOKEN_URL=https://oauth-provider.com/token

// ์„ค์ • ๋กœ๋“œ (Node.js)
require('dotenv').config();

const oauthConfig = {
    clientId: process.env.OAUTH_CLIENT_ID,
    clientSecret: process.env.OAUTH_CLIENT_SECRET,
    redirectUri: process.env.OAUTH_REDIRECT_URI,
    authorizationUrl: process.env.OAUTH_AUTHORIZATION_URL,
    tokenUrl: process.env.OAUTH_TOKEN_URL
};

// ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ๋ง ๊ฒƒ!
๐Ÿ”น ์—๋Ÿฌ ์ฒ˜๋ฆฌ

OAuth ์ธ์ฆ ๊ณผ์ •์—์„œ ๋‹ค์–‘ํ•œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด. ์ ์ ˆํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ์ค‘์š”ํ•ด. โš ๏ธ

// ํฌ๊ด„์ ์ธ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์˜ˆ์‹œ
app.get('/callback', async (req, res) => {
    try {
        // OAuth ์—๋Ÿฌ ํ™•์ธ
        if (req.query.error) {
            const errorType = req.query.error;
            const errorDescription = req.query.error_description;
            
            // ์—๋Ÿฌ ํƒ€์ž…๋ณ„ ์ฒ˜๋ฆฌ
            switch(errorType) {
                case 'access_denied':
                    // ์‚ฌ์šฉ์ž๊ฐ€ ๊ถŒํ•œ ๋ถ€์—ฌ ๊ฑฐ๋ถ€
                    return res.redirect('/login?error=access_denied');
                    
                case 'invalid_request':
                    // ์ž˜๋ชป๋œ ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ
                    console.error('Invalid OAuth request:', errorDescription);
                    return res.status(400).send('Invalid request');
                    
                case 'unauthorized_client':
                    // ํด๋ผ์ด์–ธํŠธ ์ธ์ฆ ์‹คํŒจ
                    console.error('Client authentication failed');
                    return res.status(401).send('Unauthorized');
                    
                case 'server_error':
                    // OAuth ์„œ๋ฒ„ ์—๋Ÿฌ
                    console.error('OAuth server error:', errorDescription);
                    return res.status(503).send('Service temporarily unavailable');
                    
                default:
                    console.error('Unknown OAuth error:', errorType, errorDescription);
                    return res.status(500).send('Authentication failed');
            }
        }
        
        // State ๊ฒ€์ฆ
        if (!req.query.state || req.query.state !== req.session.oauthState) {
            throw new Error('Invalid state parameter - possible CSRF attack');
        }
        
        // ์ธ๊ฐ€ ์ฝ”๋“œ ํ™•์ธ
        if (!req.query.code) {
            throw new Error('Authorization code not received');
        }
        
        // ํ† ํฐ ๊ตํ™˜
        const tokens = await exchangeCodeForToken(req.query.code);
        
        // ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ
        const userInfo = await getUserInfo(tokens.access_token);
        
        // ์„ธ์…˜ ์ €์žฅ
        req.session.user = userInfo;
        req.session.accessToken = tokens.access_token;
        
        res.redirect('/dashboard');
        
    } catch (error) {
        // ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์—๋Ÿฌ
        console.error('OAuth callback error:', error);
        
        // ์‚ฌ์šฉ์ž์—๊ฒŒ ์นœ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
        res.status(500).render('error', {
            message: '๋กœ๊ทธ์ธ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
            details: process.env.NODE_ENV === 'development' ? error.message : null
        });
    }
});

// ํ† ํฐ ๊ตํ™˜ ํ•จ์ˆ˜ (์žฌ์‹œ๋„ ๋กœ์ง ํฌํ•จ)
async function exchangeCodeForToken(code, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            const response = await axios.post(tokenUrl, {
                grant_type: 'authorization_code',
                code: code,
                redirect_uri: redirectUri,
                client_id: clientId,
                client_secret: clientSecret
            }, {
                timeout: 5000 // 5์ดˆ ํƒ€์ž„์•„์›ƒ
            });
            
            return response.data;
            
        } catch (error) {
            if (i === retries - 1) {
                // ๋งˆ์ง€๋ง‰ ์‹œ๋„ ์‹คํŒจ
                throw error;
            }
            
            // ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ์ธ ๊ฒฝ์šฐ ์žฌ์‹œ๋„
            if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
                console.log(`Token exchange retry ${i + 1}/${retries}`);
                await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
                continue;
            }
            
            // ๋‹ค๋ฅธ ์—๋Ÿฌ๋Š” ์ฆ‰์‹œ throw
            throw error;
        }
    }
}
๐Ÿ”น ๋กœ๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง

OAuth ์ธ์ฆ ๊ณผ์ •์„ ์ ์ ˆํžˆ ๋กœ๊น…ํ•˜๊ณ  ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ํ•˜์ง€๋งŒ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์ ˆ๋Œ€ ๋กœ๊ทธ์— ๋‚จ๊ธฐ๋ฉด ์•ˆ ๋ผ! ๐Ÿ“Š

// ์•ˆ์ „ํ•œ ๋กœ๊น… ์˜ˆ์‹œ
const winston = require('winston');

// ๋ฏผ๊ฐํ•œ ์ •๋ณด ํ•„ํ„ฐ๋ง
const sensitiveFields = ['access_token', 'refresh_token', 'client_secret', 'code_verifier'];

function sanitizeLog(obj) {
    const sanitized = { ...obj };
    for (const field of sensitiveFields) {
        if (sanitized[field]) {
            sanitized[field] = '[REDACTED]';
        }
    }
    return sanitized;
}

// ๋กœ๊ฑฐ ์„ค์ •
const logger = winston.createLogger({
    level: 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    transports: [
        new winston.transports.File({ filename: 'oauth-error.log', level: 'error' }),
        new winston.transports.File({ filename: 'oauth-combined.log' })
    ]
});

// OAuth ์ด๋ฒคํŠธ ๋กœ๊น…
function logOAuthEvent(event, data) {
    logger.info({
        event: event,
        timestamp: new Date().toISOString(),
        data: sanitizeLog(data)
    });
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
app.get('/login', (req, res) => {
    const state = generateState();
    const codeVerifier = generateCodeVerifier();
    
    // ๋กœ๊ทธ ๊ธฐ๋ก (๋ฏผ๊ฐํ•œ ์ •๋ณด ์ œ์™ธ)
    logOAuthEvent('oauth_login_initiated', {
        userId: req.session.userId,
        clientId: OAUTH_CONFIG.clientId,
        scope: OAUTH_CONFIG.scope,
        state: state.substring(0, 8) + '...' // ์ผ๋ถ€๋งŒ ๊ธฐ๋ก
    });
    
    // ... OAuth ๋กœ์ง
});

// ์ด์ƒ ํ–‰์œ„ ํƒ์ง€
function detectAnomalousActivity(userId, event) {
    // ์งง์€ ์‹œ๊ฐ„ ๋‚ด ๊ณผ๋„ํ•œ ์š”์ฒญ
    const recentAttempts = getRecentLoginAttempts(userId, 300); // 5๋ถ„
    
    if (recentAttempts > 10) {
        logger.warn({
            event: 'suspicious_activity',
            userId: userId,
            attempts: recentAttempts,
            message: 'Excessive login attempts detected'
        });
        
        // ์•Œ๋ฆผ ์ „์†ก ๋˜๋Š” ๊ณ„์ • ์ผ์‹œ ์ž ๊ธˆ
        sendSecurityAlert(userId);
        return true;
    }
    
    return false;
}
๐Ÿ’ก ์žฌ๋Šฅ๋„ท์—์„œ์˜ ํ™œ์šฉ ์žฌ๋Šฅ๋„ท(https://www.jaenung.net)์—์„œ OAuth 2.0 ๊ตฌํ˜„ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋ผ๋ฉด, ์ด๋Ÿฐ ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ์ ์šฉํ•ด์„œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์•ˆ์ „ํ•œ ์†”๋ฃจ์…˜์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด. ๋ณด์•ˆ์ด ์ž˜ ๊ตฌํ˜„๋œ ์„œ๋น„์Šค๋Š” ์‹ ๋ขฐ๋„๊ฐ€ ๋†’์•„์ง€๊ณ , ๋” ๋งŽ์€ ์˜๋ขฐ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์ง€!

๐ŸŽ“ OAuth 2.0 ํ•™์Šต ๋ฆฌ์†Œ์Šค

๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด ๋ฆฌ์†Œ์Šค๋“ค์„ ์ถ”์ฒœํ•ด! ๐Ÿ“–

๐Ÿ“š ๊ณต์‹ ๋ฌธ์„œ ๋ฐ RFC โ€ข RFC 6749: OAuth 2.0 Authorization Framework
โ€ข RFC 6750: OAuth 2.0 Bearer Token Usage
โ€ข RFC 7636: PKCE (Proof Key for Code Exchange)
โ€ข RFC 8252: OAuth 2.0 for Native Apps
โ€ข OAuth 2.0 Security Best Current Practice

๐ŸŒ ์˜จ๋ผ์ธ ๋ฆฌ์†Œ์Šค
โ€ข OAuth.net: ๊ณต์‹ OAuth ์ปค๋ฎค๋‹ˆํ‹ฐ ์‚ฌ์ดํŠธ
โ€ข Auth0 Blog: ์‹ค์šฉ์ ์ธ OAuth ๊ฐ€์ด๋“œ
โ€ข OWASP OAuth 2.0 Cheat Sheet
โ€ข Google OAuth 2.0 Playground: ์‹ค์Šต ๋„๊ตฌ

๐Ÿ“บ ๋™์˜์ƒ ๊ฐ•์˜
โ€ข "OAuth 2.0 and OpenID Connect (in plain English)" by Nate Barbettini
โ€ข Udemy, Coursera์˜ OAuth ๊ด€๋ จ ๊ฐ•์˜

๐Ÿ“– ์ถ”์ฒœ ๋„์„œ
โ€ข "OAuth 2 in Action" by Justin Richer and Antonio Sanso
โ€ข "Identity and Data Security for Web Development" by Jonathan LeBlanc and Tim Messerschmidt

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

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

โœจ ํ•ต์‹ฌ ์š”์•ฝ 1. OAuth 2.0์˜ ๋ณธ์งˆ
โ€ข ์ธ๊ฐ€(Authorization) ํ”„๋ ˆ์ž„์›Œํฌ
โ€ข ์‚ฌ์šฉ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๊ณต์œ ํ•˜์ง€ ์•Š๊ณ  ๋ฆฌ์†Œ์Šค ์ ‘๊ทผ ๊ถŒํ•œ ๋ถ€์—ฌ
โ€ข 4๊ฐ€์ง€ ๊ถŒํ•œ ๋ถ€์—ฌ ๋ฐฉ์‹ ์ œ๊ณต

2. ์ฃผ์š” ๋ณด์•ˆ ์œ„ํ˜‘
โ€ข ์ธ๊ฐ€ ์ฝ”๋“œ ๊ฐ€๋กœ์ฑ„๊ธฐ
โ€ข ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ์กฐ์ž‘
โ€ข ํ† ํฐ ์œ ์ถœ
โ€ข CSRF ๊ณต๊ฒฉ
โ€ข ํด๋ผ์ด์–ธํŠธ ์œ„์žฅ

3. ํ•„์ˆ˜ ๋ณด์•ˆ ๋Œ€์ฑ…
โ€ข PKCE ์‚ฌ์šฉ (๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ)
โ€ข State ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ
โ€ข HTTPS ํ•„์ˆ˜
โ€ข ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ์ •ํ™•ํ•œ ๋งค์นญ
โ€ข ์งง์€ ํ† ํฐ ์œ ํšจ๊ธฐ๊ฐ„
โ€ข ์ตœ์†Œ ๊ถŒํ•œ ์›์น™

4. ์‹ค๋ฌด ํŒ
โ€ข ๊ฒ€์ฆ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ
โ€ข ์ ์ ˆํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
โ€ข ์•ˆ์ „ํ•œ ๋กœ๊น…
โ€ข ์ •๊ธฐ์ ์ธ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ
โ€ข ์ตœ์‹  ๋ณด์•ˆ ํ‘œ์ค€ ์ ์šฉ

OAuth 2.0์€ ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ, ๊ธฐ๋ณธ ์›๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๊ณ  ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ๋”ฐ๋ฅด๋ฉด ์•ˆ์ „ํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿ”

๋ณด์•ˆ์€ ํ•œ ๋ฒˆ์— ์™„์„ฑ๋˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ์ง€์†์ ์ธ ๊ฐœ์„  ๊ณผ์ •์ด์•ผ. ์ƒˆ๋กœ์šด ์œ„ํ˜‘์ด ๋ฐœ๊ฒฌ๋˜๋ฉด ๋Œ€์‘ ๋ฐฉ๋ฒ•๋„ ์ง„ํ™”ํ•˜์ง€. ํ•ญ์ƒ ์ตœ์‹  ๋ณด์•ˆ ๋™ํ–ฅ์„ ์ฃผ์‹œํ•˜๊ณ , ์ •๊ธฐ์ ์œผ๋กœ ์‹œ์Šคํ…œ์„ ์ ๊ฒ€ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด.

์ด ๊ธ€์ด OAuth 2.0์„ ์ดํ•ดํ•˜๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜์—ˆ๊ธธ ๋ฐ”๋ผ! ๐Ÿš€
๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“ ์ง€ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜๊ฑฐ๋‚˜ ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•ด๋ด. ํ•จ๊ป˜ ๋ฐฐ์šฐ๊ณ  ์„ฑ์žฅํ•˜๋Š” ๊ฒŒ ๊ฐœ๋ฐœ์ž์˜ ๋งค๋ ฅ์ด๋‹ˆ๊นŒ! ๐Ÿ’ช

๐ŸŽ‰ Happy Coding! ๐ŸŽ‰

์•ˆ์ „ํ•˜๊ณ  ๋ฉ‹์ง„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค์–ด๋ณด์ž!

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

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

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.