๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

OAuth 2.0์˜ ๋ณด์•ˆ ์ด์Šˆ์™€ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ: ์•ˆ์ „ํ•œ ์ธ์ฆ ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ

mrmount 2024. 10. 20.

 

 

 

OAuth 2.0์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ณด์•ˆ ์ด์Šˆ

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

 


 

1. ํ† ํฐ ํƒˆ์ทจ ๋ฐ ์žฌ์‚ฌ์šฉ ๊ณต๊ฒฉ

 

1.1 ํ† ํฐ ํƒˆ์ทจ(Token Hijacking)

ํ† ํฐ ํƒˆ์ทจ ๋Š” ์•…์˜์ ์ธ ์‚ฌ์šฉ์ž๊ฐ€ ์œ ํšจํ•œ Access Token ๋˜๋Š” Refresh Token ์„ ํƒˆ์ทจํ•ด ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์˜ ๊ถŒํ•œ์œผ๋กœ API์— ์ ‘๊ทผ ํ•˜๋Š” ๊ณต๊ฒฉ์ž…๋‹ˆ๋‹ค.

 

์˜ˆ์‹œ: ํ† ํฐ ํƒˆ์ทจ ๊ณต๊ฒฉ ํ๋ฆ„

  1. ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•œ ํ›„ Access Token ์„ ๋ถ€์—ฌ๋ฐ›์Šต๋‹ˆ๋‹ค.
  2. ๋„คํŠธ์›Œํฌ ๊ณต๊ฒฉ์ž๊ฐ€ ์ค‘๊ฐ„์ž ๊ณต๊ฒฉ(MITM) ์„ ํ†ตํ•ด ํ•ด๋‹น ํ† ํฐ์„ ๊ฐ€๋กœ์ฑ•๋‹ˆ๋‹ค.
  3. ๊ณต๊ฒฉ์ž๋Š” ํƒˆ์ทจํ•œ ํ† ํฐ์œผ๋กœ API์— ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:
- HTTPS๋ฅผ ์‚ฌ์šฉ ํ•ด ๋ชจ๋“  ํ†ต์‹ ์„ ์•”ํ˜ธํ™”ํ•ฉ๋‹ˆ๋‹ค.
- ํ† ํฐ์˜ ์œ ํšจ ๊ธฐ๊ฐ„ ์„ ์งง๊ฒŒ ์„ค์ •ํ•˜๊ณ , ์ฃผ๊ธฐ์ ์œผ๋กœ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค.

 


 

2. PKCE(Proof Key for Code Exchange)์˜ ์ค‘์š”์„ฑ

 

PKCE๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

PKCE(Proof Key for Code Exchange) ๋Š” ๊ณต๊ฐœ ํด๋ผ์ด์–ธํŠธ (์˜ˆ: ๋ชจ๋ฐ”์ผ ์•ฑ)์—์„œ Authorization Code Grant ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์ฝ”๋“œ ํƒˆ์ทจ ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ ํ•˜๋Š” ๋ณด์•ˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ž…๋‹ˆ๋‹ค.

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

  1. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ฝ”๋“œ ์ฑŒ๋ฆฐ์ง€(Code Challenge) ๋ฅผ ์ƒ์„ฑํ•ด ๊ถŒํ•œ ๋ถ€์—ฌ ์š”์ฒญ์— ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
  2. ๊ถŒํ•œ ๋ถ€์—ฌ ์„œ๋ฒ„๋Š” ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ Authorization Code ๋ฅผ ๋ฐ›์„ ๋•Œ ์ฝ”๋“œ ์ฑŒ๋ฆฐ์ง€ ์™€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค.
  3. ์ผ์น˜ํ•  ๊ฒฝ์šฐ์—๋งŒ Access Token ์„ ๋ฐœ๊ธ‰ํ•ฉ๋‹ˆ๋‹ค.

 

PKCE ์ ์šฉ ์˜ˆ์ œ

import hashlib
import base64

def generate_code_challenge(code_verifier):
    challenge = hashlib.sha256(code_verifier.encode()).digest()
    return base64.urlsafe_b64encode(challenge).rstrip(b'=').decode()

code_verifier = "random_string_for_verification"
code_challenge = generate_code_challenge(code_verifier)
print(f"Code Challenge: {code_challenge}")

 

 

OAuth 2.0์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ณด์•ˆ ์ด์Šˆ

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

 


 

1. ํ† ํฐ ํƒˆ์ทจ ๋ฐ ์žฌ์‚ฌ์šฉ ๊ณต๊ฒฉ

 

1.1 ํ† ํฐ ํƒˆ์ทจ(Token Hijacking)

ํ† ํฐ ํƒˆ์ทจ ๋Š” ์•…์˜์ ์ธ ์‚ฌ์šฉ์ž๊ฐ€ ์œ ํšจํ•œ Access Token ๋˜๋Š” Refresh Token ์„ ํƒˆ์ทจํ•ด ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์˜ ๊ถŒํ•œ์œผ๋กœ API์— ์ ‘๊ทผ ํ•˜๋Š” ๊ณต๊ฒฉ์ž…๋‹ˆ๋‹ค.

 

์˜ˆ์‹œ: ํ† ํฐ ํƒˆ์ทจ ๊ณต๊ฒฉ ํ๋ฆ„

  1. ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•œ ํ›„ Access Token ์„ ๋ถ€์—ฌ๋ฐ›์Šต๋‹ˆ๋‹ค.
  2. ๋„คํŠธ์›Œํฌ ๊ณต๊ฒฉ์ž๊ฐ€ ์ค‘๊ฐ„์ž ๊ณต๊ฒฉ(MITM) ์„ ํ†ตํ•ด ํ•ด๋‹น ํ† ํฐ์„ ๊ฐ€๋กœ์ฑ•๋‹ˆ๋‹ค.
  3. ๊ณต๊ฒฉ์ž๋Š” ํƒˆ์ทจํ•œ ํ† ํฐ์œผ๋กœ API์— ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:
- HTTPS๋ฅผ ์‚ฌ์šฉ ํ•ด ๋ชจ๋“  ํ†ต์‹ ์„ ์•”ํ˜ธํ™”ํ•ฉ๋‹ˆ๋‹ค.
- ํ† ํฐ์˜ ์œ ํšจ ๊ธฐ๊ฐ„ ์„ ์งง๊ฒŒ ์„ค์ •ํ•˜๊ณ , ์ฃผ๊ธฐ์ ์œผ๋กœ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค.

 


 

2. PKCE(Proof Key for Code Exchange)์˜ ์ค‘์š”์„ฑ

 

PKCE๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

PKCE(Proof Key for Code Exchange) ๋Š” ๊ณต๊ฐœ ํด๋ผ์ด์–ธํŠธ (์˜ˆ: ๋ชจ๋ฐ”์ผ ์•ฑ)์—์„œ Authorization Code Grant ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์ฝ”๋“œ ํƒˆ์ทจ ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ ํ•˜๋Š” ๋ณด์•ˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ž…๋‹ˆ๋‹ค.

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

  1. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ฝ”๋“œ ์ฑŒ๋ฆฐ์ง€(Code Challenge) ๋ฅผ ์ƒ์„ฑํ•ด ๊ถŒํ•œ ๋ถ€์—ฌ ์š”์ฒญ์— ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
  2. ๊ถŒํ•œ ๋ถ€์—ฌ ์„œ๋ฒ„๋Š” ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ Authorization Code ๋ฅผ ๋ฐ›์„ ๋•Œ ์ฝ”๋“œ ์ฑŒ๋ฆฐ์ง€ ์™€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค.
  3. ์ผ์น˜ํ•  ๊ฒฝ์šฐ์—๋งŒ Access Token ์„ ๋ฐœ๊ธ‰ํ•ฉ๋‹ˆ๋‹ค.

 

PKCE ์ ์šฉ ์˜ˆ์ œ

import hashlib
import base64

def generate_code_challenge(code_verifier):
    challenge = hashlib.sha256(code_verifier.encode()).digest()
    return base64.urlsafe_b64encode(challenge).rstrip(b'=').decode()

code_verifier = "random_string_for_verification"
code_challenge = generate_code_challenge(code_verifier)
print(f"Code Challenge: {code_challenge}")

์„ค๋ช…:
- ์ฝ”๋“œ ์ฑŒ๋ฆฐ์ง€ ๋Š” ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„์˜ ์ผํšŒ์„ฑ ๊ฒ€์ฆ ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜์—ฌ ์ฝ”๋“œ ํƒˆ์ทจ ๊ณต๊ฒฉ ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

 


 

3. OAuth ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค์™€ ๋Œ€์‘ ๋ฐฉ๋ฒ•

๊ณต๊ฒฉ ์œ ํ˜• ์„ค๋ช… ๋Œ€์‘ ๋ฐฉ๋ฒ•
์ค‘๊ฐ„์ž ๊ณต๊ฒฉ(MITM) ๋„คํŠธ์›Œํฌ ์ƒ์—์„œ ํ† ํฐ์„ ๊ฐ€๋กœ์ฑ„๋Š” ๊ณต๊ฒฉ HTTPS ์‚ฌ์šฉ ์œผ๋กœ ํ†ต์‹  ์•”ํ˜ธํ™”
Redirect URI ์กฐ์ž‘ ๊ณต๊ฒฉ์ž๊ฐ€ Redirect URI๋ฅผ ์œ„์กฐํ•ด ํ† ํฐ์„ ํƒˆ์ทจ ์—„๊ฒฉํ•œ URI ๊ฒ€์ฆ ๊ณผ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ์ ์šฉ
CSRF ๊ณต๊ฒฉ ์‚ฌ์šฉ์ž๊ฐ€ ์˜๋„ํ•˜์ง€ ์•Š์€ ์š”์ฒญ์„ ์„œ๋ฒ„์— ์ „๋‹ฌ ์ƒํƒœ ํ† ํฐ(State Token) ์„ ์‚ฌ์šฉํ•ด CSRF ๋ฐฉ์ง€
ํ† ํฐ ์žฌ์‚ฌ์šฉ ๊ณต๊ฒฉ ํƒˆ์ทจํ•œ ํ† ํฐ์„ ๋ฐ˜๋ณต ์‚ฌ์šฉ ์งง์€ ์œ ํšจ ์‹œ๊ฐ„ ๊ณผ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ์‚ฌ์šฉ




 

4. CSRF(State Token)์™€ ํ† ํฐ ๋ฌดํšจํ™” ์ „๋žต

 

CSRF(State Token) ์‚ฌ์šฉ

OAuth 2.0์—์„œ๋Š” ์ƒํƒœ ํ† ํฐ(State Token) ์„ ์‚ฌ์šฉํ•ด CSRF ๊ณต๊ฒฉ ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ƒํƒœ ํ† ํฐ์€ ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„ ์„ธ์…˜์˜ ๋ฌด๊ฒฐ์„ฑ์„ ๊ฒ€์ฆ ํ•ฉ๋‹ˆ๋‹ค.

 

CSRF ๋ฐฉ์ง€ ์˜ˆ์ œ ์ฝ”๋“œ

import os
from flask import Flask, session, redirect, request

app = Flask(__name__)
app.secret_key = os.urandom(24)

@app.route('/login')
def login():
    session['state'] = os.urandom(24).hex()
    auth_url = f"https://accounts.google.com/o/oauth2/auth?response_type=code&state={session['state']}"
    return redirect(auth_url)

@app.route('/callback')
def callback():
    if request.args.get('state') != session['state']:
        return "CSRF ๊ณต๊ฒฉ ๊ฐ์ง€", 400
    return "๋กœ๊ทธ์ธ ์„ฑ๊ณต"

if __name__ == '__main__':
    app.run(debug=True)

์„ค๋ช…:
- ์ƒํƒœ ํ† ํฐ ์„ ์„ธ์…˜์— ์ €์žฅํ•˜๊ณ , ์ฝœ๋ฐฑ ์‹œ ๊ฒ€์ฆํ•ด CSRF ๊ณต๊ฒฉ ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

ํ† ํฐ ๋ฌดํšจํ™” ์ „๋žต

  • ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ๋ฅผ ์‚ฌ์šฉํ•ด ๋งŒ๋ฃŒ๋œ ํ† ํฐ์˜ ์žฌ์‚ฌ์šฉ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์•„์›ƒํ•  ๋•Œ ๋ชจ๋“  ํ† ํฐ์„ ๋ฌดํšจํ™” ํ•ฉ๋‹ˆ๋‹ค.

 


 

5. ์ตœ์‹  ๋ณด์•ˆ ํŠธ๋ Œ๋“œ์™€ OAuth 2.1

  • OAuth 2.1 ์€ Implicit Grant ๋ฐฉ์‹์„ ์ œ๊ฑฐํ•ด ํ† ํฐ ๋…ธ์ถœ ์œ„ํ—˜ ์„ ์ค„์˜€์Šต๋‹ˆ๋‹ค.
  • ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์—์„œ PKCE ์‚ฌ์šฉ์ด ๊ถŒ์žฅ ๋˜๋ฉฐ, ํŠนํžˆ ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„œ๋Š” ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค.
  • Refresh Token์˜ ๋ฌด๊ธฐํ•œ ์‚ฌ์šฉ์„ ์ง€์–‘ ํ•˜๊ณ , ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ ์ž๋™ ์žฌ๋ฐœ๊ธ‰์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

 


 

FAQ

Q1. OAuth 2.0์—์„œ PKCE๋Š” ์™œ ์ค‘์š”ํ•œ๊ฐ€์š”?
A1. PKCE๋Š” ์ฝ”๋“œ ํƒˆ์ทจ ๊ณต๊ฒฉ ์„ ๋ฐฉ์ง€ํ•˜๋Š” ๋ฐ ์ค‘์š”ํ•œ ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ ๋ชจ๋ฐ”์ผ ์•ฑ ์—์„œ๋Š” ํ•„์ˆ˜๋กœ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Q2. ํ† ํฐ ์œ ํšจ ๊ธฐ๊ฐ„์€ ์–ด๋–ป๊ฒŒ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‚˜์š”?
A2. Access Token ์€ ์งง๊ฒŒ(์˜ˆ: 10~30๋ถ„), Refresh Token ์€ ๋น„๊ต์  ๊ธธ๊ฒŒ ์„ค์ •ํ•ด ๋ณด์•ˆ๊ณผ ํŽธ์˜์„ฑ ์„ ๊ท ํ˜• ์žˆ๊ฒŒ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

Q3. Redirect URI๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ•˜๋‚˜์š”?
A3. ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๋ฐฉ์‹ ์œผ๋กœ ์Šน์ธ๋œ URI๋งŒ ํ—ˆ์šฉํ•˜๊ณ , ์ž„์˜์˜ URI๋กœ ๋ฆฌ๋””๋ ‰์…˜๋˜์ง€ ์•Š๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

Q4. Refresh Token์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋ฌด์—‡์ธ๊ฐ€์š”?
A4. HTTPS๋ฅผ ํ†ตํ•ด ์ „์†ก ํ•˜๊ณ , ์„œ๋ฒ„ ์ธก์— ์•”ํ˜ธํ™”๋œ ์ƒํƒœ๋กœ ์ €์žฅ ํ•ฉ๋‹ˆ๋‹ค.

Q5. OAuth 2.1์—์„œ๋Š” ์–ด๋–ค ๋ณ€ํ™”๊ฐ€ ์žˆ๋‚˜์š”?
A5. OAuth 2.1์—์„œ๋Š” Implicit Grant ์ œ๊ฑฐ , PKCE ํ•„์ˆ˜ํ™” , ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ ์ž๋™ ๊ฐฑ์‹  ๊ถŒ์žฅ ๊ณผ ๊ฐ™์€ ๋ณด์•ˆ ๊ฐœ์„ ์ด ์ด๋ฃจ์–ด์กŒ์Šต๋‹ˆ๋‹ค.

 


๋Œ“๊ธ€