OAuth 2.0์ ๋ณด์ ์ด์์ ํด๊ฒฐ ๋ฐฉ์: ์์ ํ ์ธ์ฆ ์์คํ ๊ตฌ์ถํ๊ธฐ
OAuth 2.0์์ ๋ฐ์ํ ์ ์๋ ๋ณด์ ์ด์
OAuth 2.0์ ํธ๋ฆฌํ ์ธ์ฆ ๋ฐ ์ธ๊ฐ ์์คํ ์ ์ ๊ณตํ์ง๋ง, ํ ํฐ ํ์ทจ์ ๊ณต๊ฒฉ ์๋๋ฆฌ์ค ์ ๋ ธ์ถ๋ ์ ์์ต๋๋ค. ์ฌ๋ฐ๋ฅธ ๋ณด์ ์ค์ ์ ํ์ง ์์ผ๋ฉด ๋ฏผ๊ฐํ ์ฌ์ฉ์ ์ ๋ณด ๊ฐ ๋ ธ์ถ๋ ์ ์์ผ๋ฉฐ, ํนํ ๋ชจ๋ฐ์ผ ๋ฐ ์น ์ ํ๋ฆฌ์ผ์ด์ ํ๊ฒฝ์์ ๋ณด์์ด ์ค์ํ ๋ฌธ์ ๊ฐ ๋ฉ๋๋ค.
1. ํ ํฐ ํ์ทจ ๋ฐ ์ฌ์ฌ์ฉ ๊ณต๊ฒฉ
1.1 ํ ํฐ ํ์ทจ(Token Hijacking)
ํ ํฐ ํ์ทจ ๋ ์ ์์ ์ธ ์ฌ์ฉ์๊ฐ ์ ํจํ Access Token ๋๋ Refresh Token ์ ํ์ทจํด ์ธ์ฆ๋ ์ฌ์ฉ์์ ๊ถํ์ผ๋ก API์ ์ ๊ทผ ํ๋ ๊ณต๊ฒฉ์ ๋๋ค.
์์: ํ ํฐ ํ์ทจ ๊ณต๊ฒฉ ํ๋ฆ
- ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ ํ Access Token ์ ๋ถ์ฌ๋ฐ์ต๋๋ค.
- ๋คํธ์ํฌ ๊ณต๊ฒฉ์๊ฐ ์ค๊ฐ์ ๊ณต๊ฒฉ(MITM) ์ ํตํด ํด๋น ํ ํฐ์ ๊ฐ๋ก์ฑ๋๋ค.
- ๊ณต๊ฒฉ์๋ ํ์ทจํ ํ ํฐ์ผ๋ก API์ ์ ๊ทผํฉ๋๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ:
- HTTPS๋ฅผ ์ฌ์ฉ ํด ๋ชจ๋ ํต์ ์ ์ํธํํฉ๋๋ค.
- ํ ํฐ์ ์ ํจ ๊ธฐ๊ฐ ์ ์งง๊ฒ ์ค์ ํ๊ณ , ์ฃผ๊ธฐ์ ์ผ๋ก ๊ฐฑ์ ํฉ๋๋ค.
2. PKCE(Proof Key for Code Exchange)์ ์ค์์ฑ
PKCE๋ ๋ฌด์์ธ๊ฐ?
PKCE(Proof Key for Code Exchange) ๋ ๊ณต๊ฐ ํด๋ผ์ด์ธํธ (์: ๋ชจ๋ฐ์ผ ์ฑ)์์ Authorization Code Grant ๋ฅผ ์ฌ์ฉํ ๋ ์ฝ๋ ํ์ทจ ๊ณต๊ฒฉ์ ๋ฐฉ์ง ํ๋ ๋ณด์ ๋ฉ์ปค๋์ฆ์ ๋๋ค.
PKCE ์๋ ์๋ฆฌ
- ํด๋ผ์ด์ธํธ๊ฐ ์ฝ๋ ์ฑ๋ฆฐ์ง(Code Challenge) ๋ฅผ ์์ฑํด ๊ถํ ๋ถ์ฌ ์์ฒญ์ ํฌํจํฉ๋๋ค.
- ๊ถํ ๋ถ์ฌ ์๋ฒ๋ ํด๋ผ์ด์ธํธ๋ก๋ถํฐ Authorization Code ๋ฅผ ๋ฐ์ ๋ ์ฝ๋ ์ฑ๋ฆฐ์ง ์ ๋น๊ตํฉ๋๋ค.
- ์ผ์นํ ๊ฒฝ์ฐ์๋ง 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์ ์ ๊ทผ ํ๋ ๊ณต๊ฒฉ์ ๋๋ค.
์์: ํ ํฐ ํ์ทจ ๊ณต๊ฒฉ ํ๋ฆ
- ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ ํ Access Token ์ ๋ถ์ฌ๋ฐ์ต๋๋ค.
- ๋คํธ์ํฌ ๊ณต๊ฒฉ์๊ฐ ์ค๊ฐ์ ๊ณต๊ฒฉ(MITM) ์ ํตํด ํด๋น ํ ํฐ์ ๊ฐ๋ก์ฑ๋๋ค.
- ๊ณต๊ฒฉ์๋ ํ์ทจํ ํ ํฐ์ผ๋ก API์ ์ ๊ทผํฉ๋๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ:
- HTTPS๋ฅผ ์ฌ์ฉ ํด ๋ชจ๋ ํต์ ์ ์ํธํํฉ๋๋ค.
- ํ ํฐ์ ์ ํจ ๊ธฐ๊ฐ ์ ์งง๊ฒ ์ค์ ํ๊ณ , ์ฃผ๊ธฐ์ ์ผ๋ก ๊ฐฑ์ ํฉ๋๋ค.
2. PKCE(Proof Key for Code Exchange)์ ์ค์์ฑ
PKCE๋ ๋ฌด์์ธ๊ฐ?
PKCE(Proof Key for Code Exchange) ๋ ๊ณต๊ฐ ํด๋ผ์ด์ธํธ (์: ๋ชจ๋ฐ์ผ ์ฑ)์์ Authorization Code Grant ๋ฅผ ์ฌ์ฉํ ๋ ์ฝ๋ ํ์ทจ ๊ณต๊ฒฉ์ ๋ฐฉ์ง ํ๋ ๋ณด์ ๋ฉ์ปค๋์ฆ์ ๋๋ค.
PKCE ์๋ ์๋ฆฌ
- ํด๋ผ์ด์ธํธ๊ฐ ์ฝ๋ ์ฑ๋ฆฐ์ง(Code Challenge) ๋ฅผ ์์ฑํด ๊ถํ ๋ถ์ฌ ์์ฒญ์ ํฌํจํฉ๋๋ค.
- ๊ถํ ๋ถ์ฌ ์๋ฒ๋ ํด๋ผ์ด์ธํธ๋ก๋ถํฐ Authorization Code ๋ฅผ ๋ฐ์ ๋ ์ฝ๋ ์ฑ๋ฆฐ์ง ์ ๋น๊ตํฉ๋๋ค.
- ์ผ์นํ ๊ฒฝ์ฐ์๋ง 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 ํ์ํ , ํ ํฐ ๋ง๋ฃ ์ ์๋ ๊ฐฑ์ ๊ถ์ฅ ๊ณผ ๊ฐ์ ๋ณด์ ๊ฐ์ ์ด ์ด๋ฃจ์ด์ก์ต๋๋ค.
๋๊ธ