#!/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())