การยืนยันตัวตน CLI แบบที่ถูกต้อง
(abgeo.dev)- CLI จำนวนมากใช้ localhost OAuth redirect เป็นค่าเริ่มต้น ซึ่งจบบนเบราว์เซอร์ในเครื่องโน้ตบุ๊กได้อย่างรวดเร็ว แต่ในสภาพแวดล้อมพัฒนาอย่าง SSH, คอนเทนเนอร์, WSL สมมติฐานเดียวกันนี้พังลงและทำให้ขั้นตอนล็อกอินค้าง
- วิธีปัจจุบันคือ CLI เปิดเซิร์ฟเวอร์ HTTP ชั่วคราวบน
127.0.0.1ส่งเบราว์เซอร์ไปยัง URL สำหรับยืนยันตัวตน แล้วผู้ให้บริการยืนยันตัวตนจะส่ง authorization code กลับมายัง local callback - RFC 8628 Device Authorization Grant ที่ถูกทำให้เป็นมาตรฐานในปี 2019 แยก CLI ที่ขอโทเค็นออกจากอุปกรณ์เบราว์เซอร์ที่ผู้ใช้ใช้ยืนยันตัวตน จึงไม่ต้องพึ่งการ bind พอร์ตหรือเบราว์เซอร์ในเครื่อง
- Device flow จะรับ
device_code,user_code,verification_uri,intervalแล้วคอย poll ไปที่/tokenเป็นระยะ พร้อมจัดการสถานะมาตรฐานอย่างauthorization_pending,slow_down,access_denied,expired_token - ถ้าเป็น CLI ตัวใหม่ ควรใช้ device flow เป็นค่าเริ่มต้น ค้นหาเอนด์พอยต์ด้วย
.well-known/openid-configurationและเก็บ refresh token ไว้ใน OS keychain ไม่ใช่ไฟล์ JSON ใต้~/.config
สิ่งที่ localhost redirect ตั้งเป็นสมมติฐาน
- การล็อกอิน CLI แบบทั่วไปทำงานอยู่บนสมมติฐานว่า เซิร์ฟเวอร์ HTTP ในเครื่อง และเบราว์เซอร์ของระบบอยู่บนเครื่องเดียวกัน
- CLI bind เซิร์ฟเวอร์ HTTP กับพอร์ตหนึ่งบน
127.0.0.1 - เปิดเบราว์เซอร์ของระบบไปยัง OAuth authorization endpoint พร้อมแนบ
redirect_uri=http://127.0.0.1:<port>/callback - เมื่อผู้ใช้ล็อกอินแล้ว ผู้ให้บริการยืนยันตัวตนจะ
302redirect authorization code ไปยัง loopback URL - เซิร์ฟเวอร์ HTTP ขนาดเล็กของ CLI จะอ่าน code แล้วนำไปแลกเป็นโทเค็นที่ token endpoint
- ส่วนใหญ่จะมี PKCE ติดมาด้วย และจากนั้นจะแสดงหน้า “ตอนนี้คุณสามารถปิดแท็บนี้ได้แล้ว”
- CLI bind เซิร์ฟเวอร์ HTTP กับพอร์ตหนึ่งบน
gcloud auth login,wrangler login,vercel loginรุ่นก่อนหน้า และ CLI ของผู้ขายหลายรายใช้วิธีนี้- Wrangler ใช้พอร์ต
8976 - gcloud ใช้
8085 - Claude Code จะเลือกพอร์ตชั่วคราวใหม่ทุกครั้งที่รัน
- Wrangler ใช้พอร์ต
- RFC 8252 แนะนำแพตเทิร์นนี้สำหรับ native app เมื่อมีเบราว์เซอร์บนเครื่อง แต่ไม่ได้ครอบคลุมกรณีที่โฮสต์ไม่มีเบราว์เซอร์
เหตุผลที่ผู้ใช้แทบไม่เห็นขั้นตอน localhost
- localhost callback เกิดขึ้นสั้นมากจนผู้ใช้ส่วนใหญ่ไม่ทันเห็น
- URL ที่ CLI พิมพ์ออกมายาว และมี redirect URI อยู่ใน query string
- ผู้ใช้ล็อกอินและกดยินยอมบนโดเมนจริงของผู้ให้บริการยืนยันตัวตน
- ผู้ให้บริการยืนยันตัวตนจะส่งเบราว์เซอร์ไปยัง localhost callback เพื่อให้ CLI อ่าน code แล้วค่อยพาไปยังหน้า “signed in” ที่ตกแต่งเรียบร้อย
- ภายนอกอาจดูเหมือน “ล็อกอินบนเว็บไซต์แล้ว CLI ก็ยืนยันตัวตนได้” แต่ความจริงแล้วสิ่งที่ค้ำทั้ง flow ไว้คือ การอยู่ร่วมกันของเซิร์ฟเวอร์ HTTP ในเครื่องกับเบราว์เซอร์
จุดที่มันพังใน SSH, คอนเทนเนอร์, WSL
- ทั้ง flow พึ่งสมมติฐานว่า เครื่องที่รัน CLI กับเครื่องที่รันเบราว์เซอร์เป็นเครื่องเดียวกัน
- ใน SSH session โฮสต์ระยะไกลไม่มีเบราว์เซอร์ และ
xdg-openอาจล้มเหลว หรืออาจเปิดเบราว์เซอร์ระยะไกลที่มองไม่เห็นในสภาพแวดล้อม X forwarding- สามารถทำ tunnel พอร์ต callback กลับไปยังโน้ตบุ๊กได้ แต่ redirect URI ที่ลงทะเบียนกับผู้ให้บริการยืนยันตัวตนต้องอนุญาตพอร์ตที่ผ่าน tunnel นั้นด้วย
- คอนเทนเนอร์ไม่มีเบราว์เซอร์ และหลายอิมเมจไม่มีแม้แต่
xdg-openหรือopen- สามารถเปิดพอร์ต callback ออกมาด้วย
-pได้ แต่ต้องรู้ก่อนว่า CLI จะจับพอร์ตไหน - มี issue ของผู้ใช้ Cloudflare CLI ที่ติดปัญหานี้ตามมาอย่างต่อเนื่อง
- สามารถเปิดพอร์ต callback ออกมาด้วย
- ใน WSL เบราว์เซอร์เปิดบน Windows แต่ loopback server รันอยู่บน Linux
- การ forward พอร์ตของ WSL2 ส่วนใหญ่ใช้ได้ แต่ไม่ใช่ทุกครั้ง
- บนเครื่องที่ใช้ร่วมกัน โปรเซสอื่นบนเครื่องเดียวกันอาจใช้
/proc/net/tcpหา listening port หรือแย่ง bind พอร์ตที่รู้จักล่วงหน้าได้- PKCE ปกป้องการแลก code แต่ไม่ได้ปกป้อง authenticated session ของตัว redirect เอง
fallback ก็บอกปัญหาด้านการออกแบบอยู่แล้ว
- CLI ที่ให้ loopback flow เป็นค่าเริ่มต้นมักมี fallback เอาไว้เผื่อกรณีมันพัง
- gcloud มี
--no-launch-browser - Wrangler จะค้าง และ workaround ที่ยอมรับกันคือเปิดเทอร์มินัลอีกตัวแล้ว
curllocalhost URL ด้วยตัวเอง claudeของ Anthropic จะพิมพ์ว่า “Paste code here if prompted” แล้วรอ- fallback เหล่านี้ในทางปฏิบัติคือ manual device flow และมันมีอยู่เพราะ flow หลักใช้ไม่ได้ในสภาพแวดล้อมที่มีการใช้งาน CLI จริง
RFC 8628 Device Authorization Grant
- RFC 8628 คือ OAuth 2.0 Device Authorization Grant สำหรับ “input-constrained devices” ที่ออกมาในปี 2019
- รวมถึงทีวี คอนโซล และ CLI
- หัวใจสำคัญคือแยกอุปกรณ์ที่ขอโทเค็นออกจากอุปกรณ์ที่ผู้ใช้ใช้ยืนยันตัวตน
- CLI จะส่ง POST ไปยัง
device_authorization_endpointของผู้ให้บริการยืนยันตัวตน- ตัวอย่างคำขอจะส่ง
client_id=my-cli&scope=openid+offline_access
- ตัวอย่างคำขอจะส่ง
- ผู้ให้บริการยืนยันตัวตนจะคืน JSON ที่มีค่าต่อไปนี้
device_codeuser_codeverification_uriverification_uri_completeexpires_ininterval
- CLI จะแสดง URL และโค้ดสั้น ๆ และหากทำได้ก็ควรแสดง QR สำหรับ
verification_uri_completeด้วย - ผู้ใช้จะเปิด URL บนอุปกรณ์ใดก็ได้ ล็อกอิน จากนั้นดู scope ที่ร้องขอและชื่อ client ตรวจสอบว่าโค้ดสั้นตรงกับที่ CLI แสดง แล้วจึงกดยืนยัน
การ polling และการจัดการสถานะมาตรฐาน
- CLI จะ poll token endpoint ทุก ๆ
intervalวินาที - grant type ที่ใช้คือ
urn:ietf:params:oauth:grant-type:device_code - RFC 8628 section 3.5 กำหนดสถานะดังนี้
authorization_pending: กำลังรอการยืนยันจากผู้ใช้slow_down: ผู้ให้บริการยืนยันตัวตนขอให้ลดความถี่ในการ polling และสเปกระบุว่าต้องเพิ่ม interval อย่างน้อย 5 วินาทีaccess_denied: ผู้ใช้ปฏิเสธexpired_token: รอนานเกินไปจนโทเค็นหมดอายุ
- ใน device flow นั้น CLI ไม่ต้อง bind พอร์ต และไม่สมมติว่าโฮสต์ที่รันมีเบราว์เซอร์อยู่
- วิธีล็อกอินแบบเดียวกันนี้ใช้ได้ทั้งบนโน้ตบุ๊ก คอนเทนเนอร์ และ CI job ที่รอการอนุมัติจากมนุษย์
ต้นทุนของการ polling และการค้นหาเอนด์พอยต์
- interval เริ่มต้นของการ polling คือ 5 วินาที
- การยืนยันตัวตนส่วนใหญ่เสร็จภายใน 1 นาที ดังนั้นการล็อกอินทั่วไปจะ poll
/tokenราว 10 ครั้งแล้วจบ - เซิร์ฟเวอร์สามารถเพิ่ม interval ผ่าน
slow_downได้ และ client ที่เขียนมาดีควรทำตาม - เมื่อเทียบกับการคงการเชื่อมต่อ WebSocket หรือ SSE ไปยัง stateful endpoint สำหรับทุก pending login แล้ว การทำ stateless polling ไปที่
/tokenนั้นง่ายกว่าและถูกกว่า - หากผู้ให้บริการยืนยันตัวตนรองรับ OpenID Connect Discovery CLI ก็สามารถดึง
device_authorization_endpointและtoken_endpointจาก.well-known/openid-configurationได้โดยไม่ต้อง hardcode URL
ความเสี่ยงด้านฟิชชิงของ device flow
- device flow มีการโจมตีแบบที่ผู้โจมตีเรียก
device_authorization_endpointของผู้ให้บริการจริงเพื่อรับuser_codeและdevice_codeแล้วหลอกให้เหยื่อกรอกโค้ด - เหยื่ออาจล็อกอินบน URL จริง ด้วยโค้ดจริง และกดยอมรับบน consent screen จริง
- จากนั้นผู้โจมตีจะ poll
/tokenด้วยdevice_codeที่ตัวเองสร้างไว้จนได้ access token - กลุ่ม threat actor จากรัสเซียทำแคมเปญนี้กับ tenant ของ M365 มาตั้งแต่เดือนสิงหาคม 2024
- Microsoft Threat Intelligence ติดตามในชื่อ Storm-2372
- Volexity ระบุว่าเป็นฝีมือของ APT29/Midnight Blizzard
- tenant ภาครัฐ กลาโหม และ NGO ในหลายทวีปได้รับผลกระทบ
การป้องกันฟิชชิงเป็นหน้าที่ของผู้ให้บริการยืนยันตัวตน
- การป้องกันฟิชชิงควรทำที่ฝั่ง ผู้ให้บริการยืนยันตัวตน ไม่ใช่ที่ CLI
- แนวทางบรรเทาที่จำเป็นมีดังนี้
- อายุหมดเขตของ
user_codeต้องสั้น - แสดงชื่อ client และตำแหน่งที่มาของคำขออย่างเด่นชัดบน verification page
- ทำ rate limiting กับความพยายามกรอกโค้ด
- ไม่เปิดเผย
verification_uri_completeเพื่อให้เหยื่อต้องกรอกโค้ดเองแทนการคลิกลิงก์ - สำหรับ tenant มูลค่าสูง ใช้ conditional access policy เพื่อบล็อก device code flow หากไม่ได้มาจาก network หรือ device ที่รู้จัก
- อายุหมดเขตของ
- หน้าที่ของ CLI คือทำตามสเปกและไม่สร้าง shortcut ที่ลัดเกินไป
- device flow เปลี่ยน local attack surface ไปเป็น social attack surface แต่การมี flow ที่ทำงานได้ในสภาพแวดล้อมมากกว่า และใช้ประโยชน์จาก mitigation ของผู้ให้บริการยืนยันตัวตน ยังคงเหมาะสมกว่า
แกนหลักของตัวอย่างการใช้งานใน Go
- การติดตั้งทั้งหมดทำได้ใน Go ด้วย
net/httpเพียงอย่างเดียวในราว 30 บรรทัด - ลำดับการทำงานมีดังนี้
- เรียก
http.PostFormไปยังDeviceAuthorizationEndpointพร้อมclient_idและscope - ถอดรหัส JSON ตอบกลับเพื่ออ่าน
DeviceCode,UserCode,VerificationURIComplete,Interval - แสดง
VerificationURICompleteและUserCodeให้ผู้ใช้ - ส่ง POST ซ้ำไปยัง
TokenEndpointพร้อมdevice_code,client_idและ device grant type - ถ้าเป็น
authorization_pendingให้รอต่อ - ถ้าเป็น
slow_downให้เพิ่ม interval อีก 5 วินาที - หากไม่มี error ให้คืน
access_tokenและrefresh_token - error อื่น ๆ ให้ถือว่าล้มเหลว
- เรียก
- เปิด capability “OAuth 2.0 Device Authorization Grant” ใน Keycloak realm หรือใช้ OpenID-certified provider ที่รองรับ grant นี้ ก็จะทำให้ device-flow login ใช้งานได้
แนวทางที่ควรเป็นค่าเริ่มต้นสำหรับ CLI ใหม่
- ควรตั้ง device flow เป็นค่าเริ่มต้น
- ควรค้นหาเอนด์พอยต์จาก
.well-known/openid-configurationโดยไม่ hardcode URL - ต้องเคารพ
intervalและslow_downอย่างเคร่งครัด - ควรเก็บ refresh token ไว้ใน OS keychain ไม่ใช่ไฟล์ JSON ใต้
~/.config - หากยังอยากมีเส้นทาง loopback สำหรับการล็อกอินแบบเร็วบนโน้ตบุ๊ก ก็ควรซ่อนไว้หลังแฟลก
--webและไม่ควรทำเป็นค่าเริ่มต้น
CLI ที่ย้ายไปแล้ว และเครื่องมือที่ยังค้างอยู่
- มี CLI ที่ใช้ device flow เป็นค่าเริ่มต้นแล้ว
gh auth loginใช้ device flow มาตั้งแต่แรก และถูกมองว่าเป็น reference implementation ที่สะอาดที่สุดตัวหนึ่งในโลกโอเพนซอร์สaws sso loginรัน device flow แบบ end-to-end กับ IAM Identity Centervercel loginย้ายไปใช้ RFC 8628 ในเดือนกันยายน 2025 แทนการล็อกอินด้วยอีเมลและแฟลก--oobแบบเดิม- Stripe CLI ไม่ได้ใช้ RFC 8628 ตรง ๆ แต่ใช้ pairing-code flow ที่ออกแบบ UX ได้ดี
- ยังมีเครื่องมือที่ใช้ loopback flow เป็นค่าเริ่มต้นและค่อยเสริม fallback แบบ paste-the-code
- Google
gcloud - Cloudflare
wrangler - Anthropic
claude
- Google
- ถ้าทุกครั้งที่ CLI ออกนอกโน้ตบุ๊กแล้วต้องพึ่ง manual paste-the-code fallback ก็ควรทำให้ fallback นั้นเป็น flow หลักไปเลย
1 ความคิดเห็น
ความคิดเห็นจาก Lobste.rs
เขียนค่อนข้างหยาบ ๆ แต่ก็น่าสนใจ ถ้าสลับ device code/link ทุก 1 นาที ก็น่าจะช่วยลดการถูกนำไปใช้ทำฟิชชิงได้บ้าง
หลังจากถูกใช้งานหนึ่งครั้งก็หยุดหมุนเวียน และผูกเซสชันนั้นไว้กับ IP หรือเบราว์เซอร์ ก็พอ
ถ้าเป็นผู้ให้บริการแบบ Microsoft ที่ให้ผู้ใช้พิมพ์โค้ดเอง หน้าแลนดิ้งเพจก็อาจแสดงคำแนะนำและคัดลอกโค้ดลงคลิปบอร์ดเพื่อหลอกให้ตกเป็นเหยื่อฟิชชิงได้ง่ายขึ้นด้วย
เป็นบทความที่ดี และเห็นด้วยว่าทุกคนควรย้ายไปใช้ RFC 8628
ฉันต้องเจอกระบวนการ CLI OAuth บนเครื่องพัฒนาระยะไกลบ่อยเกินไป จนทำเครื่องมือส่วนตัวขึ้นมาเพื่อกลบประสบการณ์ใช้งานที่แย่ ด้วยการดัก
xdg-openและทำ auto port forwarding: https://github.com/phinze/bankshotน่าสนใจ พอดีเมื่อไม่นานมานี้ฉันเพิ่งทำ RFC 8252 ซึ่งเป็นวิธี auth แบบ “เก่า” และไม่รู้เลยว่ามี RFC 8268 ซึ่งเป็นวิธี “ใหม่” อยู่
ดูเหมือนว่าช่องว่างความรู้นี้จะเกิดจาก use case หลักของฉันเป็นการยืนยันตัวตนกับเซิร์ฟเวอร์ของ Google เอกสารที่ฉันคิดว่าเป็นโฟลว์ RFC 8268 ระบุไว้แบบนี้
ข้อจำกัดด้าน scope ของ Google เป็นส่วนที่ OIDC โผล่มาอย่างยุ่งยาก ตามอุดมคติแล้ว Google ควรส่งคืน ID token แทนที่จะยัดทุกอย่างรวมไว้ใน access token แต่ปัญหานั้นเป็นเรื่องการตั้งค่า OAuth ของ Google ไม่ใช่คุณลักษณะของ 8628 เอง
ความซับซ้อนที่ไม่มีวันจบของ OAuth มาจากตรงนี้ มาตรฐานนิยามกรอบสำหรับ จะสร้างและส่งมอบ รูปแบบการมอบสิทธิ์อย่างไรไว้ได้ดี แต่ตั้งใจไม่แตะเลยว่ามัน ควรเป็นอะไร กว่าจะได้ชุด HTTP endpoint ร่วมกันที่ผู้ให้บริการ “ส่วนใหญ่” เห็นพ้องกัน ก็ต้องรอให้ OIDC ถูกคิดขึ้นและผ่านไปอีกหลายปี
แฮ็กอีกแบบหนึ่งคือ forward การเรียก xdg-open บนเซิร์ฟเวอร์ไปที่แล็ปท็อป ฉันทำเครื่องมือเล็ก ๆ สำหรับอินฟราส่วนตัวเพื่อทำแบบนั้นไว้: https://github.com/zimbatm/subportal/
เอาสองแนวทางนี้มารวมกันไม่ได้หรือ? คือรีไดเร็กต์ไปที่ URL แบบ
localhostแล้วให้ส่งhelloกลับมา จากนั้นถ้าไคลเอนต์ไม่ได้รับhelloก็ให้ CLI แสดง URL ออกมาพร้อมกันนั้น ถ้าเซิร์ฟเวอร์ไม่ได้รับการตอบกลับของ
helloก็ให้เบราว์เซอร์แสดงโค้ดและข้อความอย่าง “ตรวจสอบว่าคุณกำลังพยายามล็อกอินอยู่หรือไม่” ก็ได้ อาจทำให้ง่ายขึ้นแบบ Google ที่แสดงตัวเลขให้เลือกบนมือถือข้อดีคือแม้ในกรณีที่ 2 คนก็มักกดลิงก์ได้ง่าย แต่จะ แชร์ OTP/โค้ด น้อยกว่าเมื่อเทียบกัน และผู้โจมตีก็ต้องคอยสอดแทรก social engineering ระหว่างการโจมตีตลอดเวลา
ถ้ามันทำงานได้ดีบนเครื่องโลคัลและไม่ต้องมีการโต้ตอบ ก็น่าจะให้ โฟลว์แบบใช้เบราว์เซอร์ เป็นค่าเริ่มต้น