1 คะแนน โดย GN⁺ 4 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • 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
    • เมื่อผู้ใช้ล็อกอินแล้ว ผู้ให้บริการยืนยันตัวตนจะ 302 redirect authorization code ไปยัง loopback URL
    • เซิร์ฟเวอร์ HTTP ขนาดเล็กของ CLI จะอ่าน code แล้วนำไปแลกเป็นโทเค็นที่ token endpoint
    • ส่วนใหญ่จะมี PKCE ติดมาด้วย และจากนั้นจะแสดงหน้า “ตอนนี้คุณสามารถปิดแท็บนี้ได้แล้ว”
  • gcloud auth login, wrangler login, vercel login รุ่นก่อนหน้า และ CLI ของผู้ขายหลายรายใช้วิธีนี้
    • Wrangler ใช้พอร์ต 8976
    • gcloud ใช้ 8085
    • Claude Code จะเลือกพอร์ตชั่วคราวใหม่ทุกครั้งที่รัน
  • 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 ที่ติดปัญหานี้ตามมาอย่างต่อเนื่อง
  • ใน 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 ที่ยอมรับกันคือเปิดเทอร์มินัลอีกตัวแล้ว curl localhost 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_code
    • user_code
    • verification_uri
    • verification_uri_complete
    • expires_in
    • interval
  • 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 Center
    • vercel 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
  • ถ้าทุกครั้งที่ CLI ออกนอกโน้ตบุ๊กแล้วต้องพึ่ง manual paste-the-code fallback ก็ควรทำให้ fallback นั้นเป็น flow หลักไปเลย

1 ความคิดเห็น

 
GN⁺ 4 시간 전
ความคิดเห็นจาก 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 ระบุไว้แบบนี้

    Alternatives

    If you are writing an app for a platform such as Android, iOS, macOS, Linux, or Windows (including the Universal Windows Platform), that has access to the browser and full input capabilities, use the OAuth 2.0 flow for mobile and desktop applications. (You should use that flow even if your app is a command-line tool without a graphical interface.)
    ก็เลยอ่านและทำตามแค่ โฟลว์ RFC 8252 แบบตรงตัว เครื่องมือของฉันเป็น CLI ก็จริง แต่ use case ใช้เฉพาะในเครื่องโลคัล เลยไม่ได้คำนึงถึงสภาพแวดล้อมแบบ SSH หรือคอนเทนเนอร์
    นอกจากนี้ ในโฟลว์ RFC 8268 นั้น Google อนุญาตเฉพาะ OAuth 2.0 scopes แบบจำกัด เท่านั้น ซึ่งอาจเป็นข้อจำกัดร้ายแรงสำหรับบางแอปพลิเคชัน

    • แก้ไขเล็กน้อย: กลับไปเช็กเลขในต้นฉบับอีกทีแล้ว เป็น RFC 8628
      ข้อจำกัดด้าน 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 ที่แสดงตัวเลขให้เลือกบนมือถือ

    cli -> server/auth?r=localhost&fallback_choices=10,20,30  
    server -> localhost/hello
    
    Case 1: hello request received, go to redirect URI on localhost  
    Case 2: server has not received a hello reply, client has not received a hello request
    - CLI displays a/the webpage url and prompts for selecting a fallback_choice
    - Webpage displays a number say `20` from choices
      - Warn in the webpage not to share this code
    - User enters/selects it on the CLI
      - solves the token copy/paste problem if choices  
    

    ข้อดีคือแม้ในกรณีที่ 2 คนก็มักกดลิงก์ได้ง่าย แต่จะ แชร์ OTP/โค้ด น้อยกว่าเมื่อเทียบกัน และผู้โจมตีก็ต้องคอยสอดแทรก social engineering ระหว่างการโจมตีตลอดเวลา

  • ถ้ามันทำงานได้ดีบนเครื่องโลคัลและไม่ต้องมีการโต้ตอบ ก็น่าจะให้ โฟลว์แบบใช้เบราว์เซอร์ เป็นค่าเริ่มต้น

    • โฟลว์นี้ก็ทำงานผ่านเบราว์เซอร์อยู่แล้วเมื่อมันใช้ได้ผล เพียงแต่ตอนล้มเหลวมันมี เส้นทางสำรองที่ดีกว่า