115 lines
4.4 KiB
Python
115 lines
4.4 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""ขอ access token แบบ PKCE (authorization code flow) จาก Keycloak dev — สำหรับทดสอบ backend ผ่าน curl/Swagger.
|
||
|
|
|
||
|
|
ทำไมต้องมีสคริปต์นี้: ทุก client ใน realm ปิด direct grants บังคับ PKCE ตาม spec
|
||
|
|
ส่วน token ของ `admin-cli` ไม่มี claim `aud` จึงเรียก backend ไม่ผ่าน (401)
|
||
|
|
|
||
|
|
วิธีใช้:
|
||
|
|
python3 scripts/dev-token.py # ปริ้น access token
|
||
|
|
python3 scripts/dev-token.py --clip # คัดลอกเข้า clipboard (macOS)
|
||
|
|
|
||
|
|
แก้ user/password ผ่าน env: DEV_TOKEN_USERNAME / DEV_TOKEN_PASSWORD (default testuser/Test1234!)
|
||
|
|
"""
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import base64
|
||
|
|
import hashlib
|
||
|
|
import http.cookiejar
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import re
|
||
|
|
import ssl
|
||
|
|
import sys
|
||
|
|
import urllib.error
|
||
|
|
import urllib.parse
|
||
|
|
import urllib.request
|
||
|
|
|
||
|
|
REALM = "https://localhost:8443/realms/word-versioning"
|
||
|
|
CLIENT_ID = "word-versioning-web"
|
||
|
|
REDIRECT_URI = "http://localhost:5173/callback" # ไม่ต้องมี server จริง — แค่ต้องตรงกับที่ลงทะเบียนไว้
|
||
|
|
CA_FILE = os.path.expanduser("~/.office-addin-dev-certs/ca.crt")
|
||
|
|
|
||
|
|
USERNAME = os.environ.get("DEV_TOKEN_USERNAME", "testuser")
|
||
|
|
PASSWORD = os.environ.get("DEV_TOKEN_PASSWORD", "Test1234!")
|
||
|
|
|
||
|
|
|
||
|
|
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||
|
|
"""หยุดที่ 302 เพื่อเก็บ authorization code จาก Location"""
|
||
|
|
|
||
|
|
def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: ARG002
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
||
|
|
parser.add_argument("--clip", action="store_true", help="คัดลอก token เข้า clipboard (pbcopy)")
|
||
|
|
args = parser.parse_args()
|
||
|
|
|
||
|
|
verifier = base64.urlsafe_b64encode(os.urandom(48)).rstrip(b"=").decode()
|
||
|
|
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
|
||
|
|
|
||
|
|
ctx = ssl.create_default_context(cafile=CA_FILE)
|
||
|
|
opener = urllib.request.build_opener(
|
||
|
|
urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()),
|
||
|
|
NoRedirect,
|
||
|
|
urllib.request.HTTPSHandler(context=ctx),
|
||
|
|
)
|
||
|
|
|
||
|
|
auth_url = f"{REALM}/protocol/openid-connect/auth?" + urllib.parse.urlencode(
|
||
|
|
{
|
||
|
|
"client_id": CLIENT_ID,
|
||
|
|
"redirect_uri": REDIRECT_URI,
|
||
|
|
"response_type": "code",
|
||
|
|
"scope": "openid",
|
||
|
|
"state": "dev-token",
|
||
|
|
"code_challenge": challenge,
|
||
|
|
"code_challenge_method": "S256",
|
||
|
|
}
|
||
|
|
)
|
||
|
|
login_page = opener.open(auth_url).read().decode()
|
||
|
|
action = re.search(r'action="([^"]+)"', login_page)
|
||
|
|
if not action:
|
||
|
|
print("ไม่พบ login form — ตรวจ Keycloak ที่ https://localhost:8443", file=sys.stderr)
|
||
|
|
return 1
|
||
|
|
|
||
|
|
credentials = urllib.parse.urlencode(
|
||
|
|
{"username": USERNAME, "password": PASSWORD, "credentialId": ""}
|
||
|
|
).encode()
|
||
|
|
try:
|
||
|
|
opener.open(urllib.request.Request(action.group(1).replace("&", "&"), data=credentials))
|
||
|
|
print("login ไม่ redirect — ตรวจ username/password", file=sys.stderr)
|
||
|
|
return 1
|
||
|
|
except urllib.error.HTTPError as error:
|
||
|
|
location = error.headers.get("Location", "")
|
||
|
|
code = urllib.parse.parse_qs(urllib.parse.urlparse(location).query).get("code", [None])[0]
|
||
|
|
if not code:
|
||
|
|
print("ไม่ได้รับ authorization code (redirect ผิดหรือ login พลาด)", file=sys.stderr)
|
||
|
|
return 1
|
||
|
|
|
||
|
|
token_body = urllib.parse.urlencode(
|
||
|
|
{
|
||
|
|
"grant_type": "authorization_code",
|
||
|
|
"client_id": CLIENT_ID,
|
||
|
|
"code": code,
|
||
|
|
"redirect_uri": REDIRECT_URI,
|
||
|
|
"code_verifier": verifier,
|
||
|
|
}
|
||
|
|
).encode()
|
||
|
|
token_response = json.loads(
|
||
|
|
opener.open(urllib.request.Request(f"{REALM}/protocol/openid-connect/token", data=token_body)).read()
|
||
|
|
)
|
||
|
|
token = token_response["access_token"]
|
||
|
|
|
||
|
|
if args.clip:
|
||
|
|
import subprocess
|
||
|
|
|
||
|
|
subprocess.run("pbcopy", input=token.encode(), check=True)
|
||
|
|
print(f"คัดลอก access token ({len(token)} chars) เข้า clipboard แล้ว")
|
||
|
|
else:
|
||
|
|
print(token)
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main())
|