Authentication and Authorization (Authentication และ Authorization)

Authentication และ authorization เป็นแนวคิดหลักเมื่อพูดถึงผู้คนและสิ่งต่างๆ ที่โต้ตอบกับระบบของเรา ในบริบทของ security authentication คือกระบวนการที่เรายืนยันว่าฝ่ายหนึ่งเป็นตัวจริงตามที่พวกเขาอ้าง โดยทั่วไปเรา authenticate ผู้ใช้ที่เป็นมนุษย์ด้วยการให้พวกเขาพิมพ์ username และ password ของตัวเอง เราสมมติว่ามีแค่ผู้ใช้ตัวจริงเท่านั้นที่มีข้อมูลนี้ ดังนั้นคนที่กรอกข้อมูลนี้ก็ต้องเป็นตัวพวกเขาเอง ยังมีระบบอื่นๆ ที่ซับซ้อนกว่านั้นด้วยเช่นกัน โทรศัพท์ของเราตอนนี้ให้เราใช้ลายนิ้วมือหรือใบหน้ายืนยันว่าเราคือใครที่เราบอกว่าเป็น โดยทั่วไป เวลาที่เราพูดถึงเรื่องนี้แบบเป็นนามธรรมว่าใครหรืออะไรกำลังถูก authenticate เราจะเรียกฝ่ายนั้นว่า principal .

Authorization คือกลไกที่เราแมปจาก principal ไปยัง action ที่เราอนุญาตให้พวกเขาทำ บ่อยครั้ง เมื่อ principal ถูก authenticate แล้ว เราจะได้รับข้อมูลเกี่ยวกับพวกเขาที่จะช่วยให้เราตัดสินใจได้ว่าเราควรอนุญาตให้พวกเขาทำอะไร เราอาจได้รับข้อมูลเช่น แผนกหรือสำนักงานที่พวกเขาทำงานอยู่—ข้อมูลชิ้นหนึ่งที่ระบบของเราสามารถใช้ตัดสินใจได้ว่า principal สามารถและไม่สามารถทำอะไรได้บ้าง

ความง่ายในการใช้งานเป็นสิ่งสำคัญ—เราต้องการทำให้ผู้ใช้ของเราเข้าถึงระบบของเราได้ง่าย เราไม่ต้องการให้ทุกคนต้องล็อกอินแยกกันเพื่อเข้าถึง microservice ที่แตกต่างกัน โดยใช้ username และ password ที่แตกต่างกันไปในแต่ละตัว ดังนั้นเราจึงต้องพิจารณาด้วยว่าเราจะ implement single sign-on (SSO) ในสภาพแวดล้อม microservice ได้อย่างไร

Service-to-Service Authentication (Authentication ระหว่าง Service)

ก่อนหน้านี้ เราได้พูดถึง mutual TLS ซึ่งนอกจากจะปกป้องข้อมูลขณะเดินทางแล้ว ยังช่วยให้เรา implement authentication รูปแบบหนึ่งได้ด้วย เมื่อ client คุยกับ server ผ่าน mutual TLS server จะสามารถ authenticate client ได้ และ client ก็สามารถ authenticate server ได้เช่นกัน—นี่คือรูปแบบหนึ่งของ service-to-service authentication ยังมี authentication scheme อื่นๆ ที่สามารถใช้ได้นอกเหนือจาก mutual TLS ตัวอย่างที่พบบ่อยคือการใช้ API key ซึ่ง client ต้องใช้ key เพื่อ hash คำขอในลักษณะที่ server สามารถยืนยันได้ว่า client ใช้ key ที่ถูกต้อง

Human Authentication (Authentication ของมนุษย์)

เรา คุ้นเคยกับการที่มนุษย์ authenticate ตัวเองด้วยการรวมกันของ username และ password ที่คุ้นเคย ปัจจุบันสิ่งนี้ถูกใช้เป็นส่วนหนึ่งของแนวทาง multifactor authentication มากขึ้นเรื่อยๆ ซึ่งผู้ใช้อาจต้องใช้ความรู้มากกว่าหนึ่งอย่าง ( factor ) เพื่อ authenticate ตัวเอง โดยทั่วไปแล้วสิ่งนี้จะอยู่ในรูปแบบของ multifactor authentication (MFA) 14 ซึ่งต้องใช้มากกว่าหนึ่ง factor MFA มักจะใช้การรวมกันของ username และ password ตามปกติ ร่วมกับการให้ factor เพิ่มเติมอีกอย่างน้อยหนึ่งอย่าง

ประเภทของ authentication factor เติบโตขึ้นในช่วงไม่กี่ปีที่ผ่านมา—ตั้งแต่รหัสที่ส่งผ่าน SMS และ magic link ที่ส่งผ่านอีเมล ไปจนถึงแอปมือถือเฉพาะทางอย่าง Authy และอุปกรณ์ฮาร์ดแวร์ USB และ NFC อย่าง YubiKey Biometric factor ก็ถูกใช้กันแพร่หลายมากขึ้นในปัจจุบันเช่นกัน เนื่องจากผู้ใช้เข้าถึงฮาร์ดแวร์ที่รองรับสิ่งอย่างการจดจำลายนิ้วมือหรือใบหน้าได้มากขึ้น แม้ MFA จะพิสูจน์ตัวเองแล้วว่าปลอดภัยกว่ามากในฐานะแนวทางทั่วไป และ public service จำนวนมากก็รองรับมัน แต่มันก็ยังไม่ได้รับความนิยมในตลาดแมสมากนัก แม้ว่าผมคาดว่าเรื่องนี้จะเปลี่ยนไปในอนาคต สำหรับการจัดการ authentication ของ service สำคัญที่จำเป็นต่อการทำงานของซอฟต์แวร์ของคุณ หรือที่อนุญาตให้เข้าถึงข้อมูลอ่อนไหวเป็นพิเศษ (เช่น การเข้าถึง source code) ผมมองว่าการใช้ MFA เป็นสิ่งที่ต้องมี

Common Single Sign-On Implementations (Implementation ทั่วไปของ Single Sign-On)

แนวทาง ทั่วไปสำหรับ authentication คือการใช้โซลูชัน single sign-on (SSO) บางรูปแบบ เพื่อให้แน่ใจว่าผู้ใช้ต้อง authenticate ตัวเองเพียงครั้งเดียวต่อ session แม้ว่าในระหว่าง session นั้นพวกเขาอาจโต้ตอบกับ downstream service หรือแอปพลิเคชันหลายตัวก็ตาม ตัวอย่างเช่น เมื่อคุณล็อกอินด้วยบัญชี Google คุณจะถูกล็อกอินเข้า Google Calendar, Gmail และ Google Docs พร้อมกัน แม้ว่าสิ่งเหล่านี้จะเป็นระบบที่แยกจากกัน

เมื่อ principal พยายามเข้าถึง resource (เช่น web-based interface) พวกเขาจะถูก redirect ไปให้ authenticate กับ identity provider identity provider อาจขอให้พวกเขาระบุ username และ password หรืออาจต้องการสิ่งที่ล้ำหน้ากว่านั้นเช่น MFA เมื่อ identity provider พอใจแล้วว่า principal ได้รับการ authenticate มันจะให้ข้อมูลไปยัง service provider เพื่อให้ service provider ตัดสินใจได้ว่าจะให้สิทธิ์เข้าถึง resource นั้นหรือไม่

Identity provider นี้อาจเป็นระบบที่ hosted ภายนอก หรือเป็นสิ่งที่อยู่ภายในองค์กรของคุณเองก็ได้ Google ยกตัวอย่างเช่น ให้บริการ OpenID Connect identity provider สำหรับองค์กรขนาดใหญ่ อย่างไรก็ตาม เป็นเรื่องปกติที่จะมี identity provider ของตัวเอง ซึ่งอาจเชื่อมโยงกับ directory service ของบริษัทของคุณ directory service อาจเป็นสิ่งอย่าง Lightweight Directory Access Protocol (LDAP) หรือ Active Directory ระบบเหล่านี้ช่วยให้คุณเก็บข้อมูลเกี่ยวกับ principal เช่นบทบาทที่พวกเขามีในองค์กร บ่อยครั้ง directory service และ identity provider เป็นตัวเดียวกัน ในขณะที่บางครั้งก็แยกจากกันแต่เชื่อมโยงกัน Okta ยกตัวอย่างเช่น เป็น hosted SAML identity provider ที่จัดการงานอย่าง two-factor authentication แต่สามารถเชื่อมโยงกับ directory service ของบริษัทคุณในฐานะแหล่งข้อมูลจริงได้

ดังนั้น identity provider จะให้ข้อมูลระบบว่า principal คือใคร แต่ระบบเองต่างหากที่ตัดสินใจว่า principal นั้นได้รับอนุญาตให้ทำอะไรได้บ้าง

SAML เป็นมาตรฐานที่ใช้ SOAP เป็นฐาน และเป็นที่รู้กันว่ามีความซับซ้อนพอสมควรในการทำงานด้วย แม้จะมี library และ tooling รองรับอยู่มากมาย และตั้งแต่ edition แรกของหนังสือเล่มนี้เป็นต้นมา มันก็ได้รับความนิยมลดลงอย่างรวดเร็ว 15 OpenID Connect เป็นมาตรฐานที่เกิดขึ้นมาในฐานะ implementation เฉพาะของ OAuth 2.0 โดยอิงตามวิธีที่ Google และผู้ให้บริการรายอื่นจัดการเรื่อง SSO มันใช้ REST call ที่เรียบง่ายกว่า และด้วยความเรียบง่ายและการรองรับที่แพร่หลาย มันจึงกลายเป็นกลไกหลักสำหรับ end-user SSO และได้รับการยอมรับอย่างมากในองค์กรต่างๆ

Single Sign-On Gateway (SSO Gateway)

เรา สามารถตัดสินใจจัดการเรื่องการ redirect ไปยัง และการ handshake กับ identity provider ในแต่ละ microservice ได้ เพื่อให้คำขอที่ยังไม่ได้ authenticate จากฝ่ายภายนอกถูกจัดการอย่างเหมาะสม แน่นอนว่านี่อาจหมายถึง functionality ที่ซ้ำซ้อนกันจำนวนมากในบรรดา microservice ของเรา shared library อาจช่วยได้ แต่ เรา ต้องระมัดระวังเพื่อหลีกเลี่ยง coupling ที่อาจเกิดจาก shared code (ดู “DRY and the Perils of Code Reuse in a Microservice World” สำหรับข้อมูลเพิ่มเติม) shared library ก็จะไม่ช่วยอะไรเช่นกันหากเรามี microservice ที่เขียนด้วย technology stack ที่แตกต่างกัน

แทนที่จะให้แต่ละ service จัดการ handshake กับ identity provider ของเราเอง แนวทางที่พบได้บ่อยกว่าคือการใช้ gateway เป็น proxy อยู่ระหว่าง service ของคุณกับโลกภายนอก (ดังที่แสดงใน Figure 11-7 ) แนวคิดคือเราสามารถรวมพฤติกรรมสำหรับ redirect ผู้ใช้และทำ handshake ไว้ที่จุดเดียวเท่านั้น

bms2 1107

Figure 11-7. การใช้ gateway เพื่อจัดการ SSO

อย่างไรก็ตาม เรายังต้องแก้ปัญหาว่า downstream service จะได้รับข้อมูลเกี่ยวกับ principal อย่างไร เช่น username หรือบทบาทที่พวกเขามี ถ้าคุณใช้ HTTP คุณสามารถ configure gateway ของคุณให้เติมข้อมูลนี้ลงใน header ได้ Shibboleth เป็นเครื่องมือหนึ่งที่ทำแบบนี้ได้ให้คุณ และผมเคยเห็นมันถูกใช้ร่วมกับ Apache web server เพื่อจัดการการเชื่อมต่อกับ SAML-based identity provider ได้อย่างมีประสิทธิภาพมาก อีกทางเลือกหนึ่ง ซึ่งเราจะดูรายละเอียดในอีกไม่ช้า คือการสร้าง JSON Web Token (JWT) ที่มีข้อมูลทั้งหมดเกี่ยวกับ principal ซึ่งมีประโยชน์หลายอย่าง รวมถึงการเป็นสิ่งที่เราสามารถส่งต่อจาก microservice หนึ่งไปยังอีกตัวหนึ่งได้ง่ายขึ้น

อีกข้อพิจารณาหนึ่งเมื่อใช้ single sign-on gateway คือ ถ้าเราตัดสินใจโยนความรับผิดชอบเรื่อง authentication ไปให้ gateway มันจะยากขึ้นในการทำความเข้าใจว่า microservice หนึ่งๆ ทำงานอย่างไรเมื่อมองมันแบบแยกเดี่ยว จำได้ไหมใน Chapter 9 ที่เราสำรวจความท้าทายบางอย่างในการจำลองสภาพแวดล้อมที่คล้าย production? ถ้าคุณตัดสินใจใช้ gateway ให้แน่ใจว่านักพัฒนาของคุณสามารถรัน service ของพวกเขาโดยอยู่หลัง gateway ได้โดยไม่ต้องใช้ความพยายามมากเกินไป

ปัญหาสุดท้ายของแนวทางนี้คือมันอาจทำให้คุณเผลอมีความรู้สึกปลอดภัยที่ผิดพลาดได้ อีกครั้ง ผมชอบกลับไปที่แนวคิด defense in depth—ตั้งแต่ network perimeter ไปจนถึง subnet, firewall, เครื่อง, ระบบปฏิบัติการ และฮาร์ดแวร์ที่อยู่เบื้องล่าง คุณมีความสามารถในการ implement มาตรการ security ที่จุดเหล่านี้ทั้งหมด ผมเคยเห็นบางคนใส่ไข่ทั้งหมดไว้ในตะกร้าใบเดียว โดยพึ่งพา gateway ให้จัดการทุกขั้นตอนแทนพวกเขา และเราก็รู้กันดีว่าเกิดอะไรขึ้นเมื่อเรามี single point of failure…

แน่นอนว่าคุณสามารถใช้ gateway นี้ทำสิ่งอื่นๆ ได้ด้วย ตัวอย่างเช่น คุณอาจตัดสินใจ terminate HTTPS ที่ระดับนี้ รัน intrusion detection และอื่นๆ ด้วย แต่ต้องระมัดระวัง gateway layer มักมีแนวโน้มที่จะรับ functionality มากขึ้นเรื่อยๆ ซึ่งในตัวมันเองก็อาจกลายเป็นจุด coupling ขนาดใหญ่ได้ และยิ่ง functionality มากเท่าไร attack surface ก็ยิ่งกว้างขึ้นเท่านั้น

Fine-Grained Authorization (Authorization แบบละเอียด)

Gateway อาจสามารถให้ coarse-grained authentication ที่มีประสิทธิภาพพอสมควร ตัวอย่างเช่น มันสามารถป้องกันไม่ให้ผู้ใช้ที่ไม่ได้ล็อกอินเข้าถึงแอปพลิเคชัน helpdesk ได้ สมมติว่า gateway ของเราสามารถดึง attribute เกี่ยวกับ principal ที่ได้มาจากการ authenticate ได้ มันอาจสามารถตัดสินใจได้ละเอียดขึ้น ตัวอย่างเช่น เป็นเรื่องปกติที่จะจัดคนไว้ในกลุ่มหรือกำหนด role ให้พวกเขา เราสามารถใช้ข้อมูลนี้เพื่อเข้าใจว่าพวกเขาสามารถทำอะไรได้บ้าง ดังนั้นสำหรับแอปพลิเคชัน helpdesk เราอาจอนุญาตให้เข้าถึงได้เฉพาะ principal ที่มี role ที่กำหนด (เช่น STAFF ) นอกเหนือจากการอนุญาต (หรือไม่อนุญาต) การเข้าถึง resource หรือ endpoint ที่เจาะจงแล้ว เราจำเป็นต้องปล่อยส่วนที่เหลือไว้ให้ microservice เอง มันจะต้องตัดสินใจต่อไปว่า operation ใดที่จะอนุญาต

กลับมาที่แอปพลิเคชัน helpdesk ของเรา: เราอนุญาตให้พนักงานคนไหนก็ได้เห็นรายละเอียดทั้งหมดหรือไม่? มีความเป็นไปได้มากกว่าที่เราจะมี role ที่แตกต่างกันในการทำงาน ตัวอย่างเช่น principal ในกลุ่ม CALL_CENTER อาจได้รับอนุญาตให้ดูข้อมูลใดๆ ก็ได้เกี่ยวกับลูกค้า ยกเว้นรายละเอียดการชำระเงิน principal นี้อาจสามารถออกเงินคืนได้เช่นกัน แต่จำนวนเงินอาจถูกจำกัดไว้ ใครก็ตามที่มี role CALL_CENTER_TEAM_LEADER อาจสามารถออกเงินคืนจำนวนมากกว่าได้

การตัดสินใจเหล่านี้จำเป็นต้องเป็นเรื่องภายใน microservice นั้นๆ ผมเคยเห็นคนใช้ attribute ต่างๆ ที่มาจาก identity provider ในลักษณะที่แย่มาก โดยใช้ role ที่ละเอียดมากๆ อย่าง CALL_CENTER_50_DOLLAR_REFUND ซึ่งพวกเขาลงเอยด้วยการใส่ข้อมูลเฉพาะของ functionality ของ microservice ตัวเดียวเข้าไปใน directory service ของตน นี่เป็นฝันร้ายในการดูแลรักษา และให้ขอบเขตน้อยมากให้ service ของเรามี life cycle ที่เป็นอิสระของตัวเอง เพราะจู่ๆ ข้อมูลก้อนหนึ่งเกี่ยวกับพฤติกรรมของ service ก็ไปอยู่ที่อื่น อาจอยู่ในระบบที่บริหารจัดการโดยส่วนงานอื่นในองค์กร

การทำให้แน่ใจว่า microservice มีข้อมูลที่จำเป็นในการประเมินคำขอ authorization แบบละเอียดขึ้นนั้นคุ้มค่าแก่การพูดถึงเพิ่มเติม—เราจะกลับมาพูดถึงเรื่องนี้อีกครั้งเมื่อเราดูเรื่อง JWT ในอีกไม่ช้า

แทนที่จะทำแบบนั้น ให้เลือกใช้ role แบบ coarse-grained ที่จำลองมาจากวิธีที่องค์กรของคุณทำงาน ย้อนกลับไปที่บทต้นๆ ของหนังสือเล่มนี้ จำได้ไหมว่าเรากำลังสร้างซอฟต์แวร์ให้ตรงกับวิธีที่องค์กรของเราทำงาน ดังนั้นให้ใช้ role ของคุณในแนวทางนี้เช่นกัน

The Confused Deputy Problem (ปัญหา Confused Deputy)

การมี principal authenticate กับระบบทั้งหมดโดยใช้สิ่งอย่าง SSO gateway ก็เพียงพอแล้ว และสิ่งนี้อาจเพียงพอที่จะควบคุมการเข้าถึง microservice ตัวหนึ่ง แต่จะเกิดอะไรขึ้นถ้า microservice นั้นต้องเรียก call เพิ่มเติมเพื่อทำ operation ให้เสร็จสมบูรณ์? สิ่งนี้อาจทำให้เราเปิดช่องให้เกิดช่องโหว่ประเภทที่รู้จักกันในชื่อ confused deputy problem สิ่งนี้เกิดขึ้นเมื่อ upstream party หลอกให้ intermediate party ทำสิ่งที่ไม่ควรทำ ลองมาดูตัวอย่างที่เป็นรูปธรรมใน Figure 11-8 ซึ่งแสดงเว็บไซต์ online shopping ของ MusicCorp browser-based JavaScript UI ของเราคุยกับ microservice Web Shop ฝั่ง server ซึ่งเป็นประเภทหนึ่งของ backend for frontend เราจะสำรวจเรื่องนี้ให้ลึกขึ้นใน “Pattern: Backend for Frontend (BFF)” แต่ในตอนนี้ ให้นึกถึงมันในฐานะ component ฝั่ง server ที่ทำ call aggregation และ filtering สำหรับ external interface ที่เจาะจง (ในกรณีของเราคือ browser-based JavaScript UI) Call ที่ทำระหว่าง browser กับ Web Shop สามารถ authenticate ได้ด้วย OpenID Connect จนถึงตรงนี้ก็ยังโอเคอยู่

bms2 1108

Figure 11-8. ตัวอย่างที่ confused deputy อาจเข้ามามีบทบาท

เมื่อผู้ใช้ล็อกอินเข้าระบบ พวกเขาสามารถคลิกลิงก์เพื่อดูรายละเอียดของคำสั่งซื้อได้ เพื่อแสดงข้อมูลนี้ เราจำเป็นต้องดึงคำสั่งซื้อเดิมกลับมาจาก service Order แต่เราก็ต้องการดูข้อมูลการจัดส่งของคำสั่งซื้อนั้นด้วย ดังนั้นเมื่อลูกค้าที่ล็อกอินอยู่คลิกลิงก์ /orderStatus/12345 คำขอนี้จะถูกส่งไปยัง Web Shop ซึ่งจากนั้นจำเป็นต้องเรียก call ไปยัง microservice Order และ Shipping เพื่อขอรายละเอียดของคำสั่งซื้อ 12345 .

แต่ downstream service เหล่านี้ควรยอมรับ call จาก Web Shop หรือไม่? เราอาจนำจุดยืนแบบ implicit trust มาใช้: เพราะ call มาจากภายใน perimeter ของเรา มันก็โอเค เราสามารถใช้ certificate หรือ API key เพื่อยืนยันได้ด้วยซ้ำว่ามันเป็น Web Shop ที่กำลังขอข้อมูลนี้จริงๆ แต่นั่นเพียงพอหรือไม่? ตัวอย่างเช่น ลูกค้าที่ล็อกอินเข้าระบบ online shopping สามารถดูรายละเอียดบัญชีของตัวเองได้ แล้วถ้าลูกค้าสามารถหลอก UI ของ web shop ให้ส่งคำขอเพื่อดูรายละเอียดของ คนอื่น ได้ล่ะ บางทีอาจด้วยการส่ง call ด้วย credential ที่ล็อกอินอยู่ของตัวเอง?

ในตัวอย่างนี้ อะไรจะหยุดลูกค้าจากการขอดูคำสั่งซื้อที่ไม่ใช่ของพวกเขาได้? เมื่อล็อกอินแล้ว พวกเขาอาจเริ่มส่งคำขอสำหรับคำสั่งซื้ออื่นๆ ที่ไม่ใช่ของตัวเองเพื่อดูว่าจะได้ข้อมูลที่มีประโยชน์หรือไม่ พวกเขาอาจเริ่มเดา order ID เพื่อดูว่าสามารถดึงข้อมูลของคนอื่นออกมาได้หรือไม่ โดยพื้นฐานแล้ว สิ่งที่เกิดขึ้นตรงนี้คือ แม้เราจะ authenticate ผู้ใช้คนนั้นแล้ว แต่เรายังไม่ได้ให้ authorization ที่เพียงพอ สิ่งที่เราต้องการคือให้บางส่วนของระบบเราสามารถตัดสินได้ว่าคำขอเพื่อดูรายละเอียดของ User A สามารถได้รับอนุญาตก็ต่อเมื่อเป็น User A เท่านั้นที่ขอดู แล้วตรรกะนี้ควรอยู่ตรงไหนล่ะ?

Centralized, Upstream Authorization (Authorization แบบรวมศูนย์ที่ Upstream)

หนึ่ง ในทางเลือกสำหรับหลีกเลี่ยงปัญหา confused deputy คือการทำ authorization ที่จำเป็นทั้งหมดทันทีที่คำขอมาถึงระบบของเรา ใน Figure 11-8 นี่หมายความว่าเราจะมุ่งทำ authorize คำขอในตัว SSO gateway เอง หรือใน Web Shop แนวคิดคือ เมื่อถึงเวลาที่ call ถูกส่งไปยัง microservice Order หรือ Shipping เราสมมติว่าคำขอเหล่านั้นได้รับอนุญาตแล้ว

รูปแบบของ upstream authorization นี้บอกเป็นนัยว่าเรากำลังยอมรับ implicit trust รูปแบบหนึ่ง (ตรงข้ามกับ zero trust)—microservice Shipping และ Order ต้องสมมติว่าพวกมันจะได้รับแค่คำขอที่พวกมันได้รับอนุญาตให้ทำเท่านั้น อีกปัญหาหนึ่งคือ upstream entity—เช่น gateway หรืออะไรทำนองนั้น—จำเป็นต้องรู้ว่า downstream microservice มี functionality อะไรบ้าง และต้องรู้วิธีจำกัดการเข้าถึง functionality นั้น

ในอุดมคติแล้ว เราต้องการให้ microservice ของเรา self-contained มากที่สุดเท่าที่จะเป็นไปได้ เพื่อให้ง่ายที่สุดในการเปลี่ยนแปลงและ rollout functionality ใหม่ เราต้องการให้การ release ของเราเรียบง่ายที่สุด—เราต้องการ independent deployability ถ้าการ deploy ตอนนี้เกี่ยวข้องทั้งการ deploy microservice ใหม่และการ apply configuration ที่เกี่ยวข้องกับ authorization ไปยัง upstream gateway นั่นก็ดูไม่ค่อย “independent” เลยสำหรับผม

ดังนั้นเราจึงต้องการผลักการตัดสินใจว่า call ควรได้รับ authorization หรือไม่ไปยัง microservice ตัวเดียวกันกับที่ functionality ที่ถูกร้องขออาศัยอยู่ วิธีนี้ทำให้ microservice self-contained มากขึ้น และยังให้ตัวเลือกในการ implement zero trust ให้เราด้วย หากเราต้องการ

Decentralizing Authorization (การกระจายศูนย์ Authorization)

เมื่อพิจารณา ความท้าทายของ centralized authorization ในสภาพแวดล้อม microservice เราอยากผลักตรรกะนี้ไปยัง downstream microservice microservice Order เป็นที่ที่ functionality สำหรับการเข้าถึงรายละเอียดคำสั่งซื้ออาศัยอยู่ ดังนั้นมันจึงสมเหตุสมผลที่ service นั้นจะเป็นผู้ตัดสินว่า call นั้นถูกต้องหรือไม่ ในกรณีนี้โดยเฉพาะ microservice Order ต้องการข้อมูลว่ามนุษย์คนใดเป็นผู้ส่งคำขอนี้มา แล้วเราจะส่งข้อมูลนี้ไปยัง microservice Order ได้อย่างไร?

ในระดับที่ง่ายที่สุด เราสามารถแค่กำหนดให้ตัวระบุ (identifier) ของคนที่ส่งคำขอถูกส่งไปยัง microservice Order ก็ได้ ถ้าใช้ HTTP ยกตัวอย่างเช่น เราสามารถแค่ใส่ username ลงใน header ได้ แต่ในกรณีแบบนั้น อะไรจะหยุดฝ่ายไม่หวังดีจากการใส่ชื่อไหนก็ได้ลงในคำขอเพื่อได้รับข้อมูลที่พวกเขาต้องการ? ในอุดมคติแล้ว เราต้องการวิธีที่มั่นใจได้ว่าคำขอนั้น เป็นการส่งในนามของ ผู้ใช้ที่ authenticate แล้วจริงๆ และเราสามารถส่งข้อมูลเพิ่มเติมเกี่ยวกับผู้ใช้คนนั้นได้—ตัวอย่างเช่น กลุ่มที่ผู้ใช้นั้นอาจอยู่ใน

ในอดีต มีวิธีการที่หลากหลายในการจัดการเรื่องนี้ (รวมถึงเทคนิคอย่าง nested SAML assertion ซึ่งใช่ มันเจ็บปวดพอๆ กับที่มันฟังดู) แต่เมื่อไม่นานมานี้ทางออกที่พบบ่อยที่สุดสำหรับปัญหานี้คือการใช้ JSON Web Token

JSON Web Tokens (JSON Web Token)

JWT ช่วยให้คุณเก็บ claim หลายอย่างเกี่ยวกับบุคคลหนึ่งลงในสตริงที่สามารถส่งต่อไปมาได้ token นี้สามารถถูก sign เพื่อให้แน่ใจว่าโครงสร้างของ token ไม่ได้ถูกปลอมแปลง และยังสามารถเข้ารหัสเพิ่มเติมได้เพื่อให้การรับประกันด้าน cryptographic เกี่ยวกับว่าใครสามารถอ่านข้อมูลได้ แม้ JWT จะสามารถใช้สำหรับการแลกเปลี่ยนข้อมูลทั่วไปในกรณีที่สำคัญที่ต้องมั่นใจว่าข้อมูลไม่ได้ถูกดัดแปลง แต่ส่วนใหญ่แล้วมันถูกใช้เพื่อช่วยส่งข้อมูลที่ช่วยสนับสนุน authorization

เมื่อถูก sign แล้ว JWT สามารถถูกส่งผ่าน protocol ที่หลากหลายได้อย่างง่ายดาย และ token สามารถถูก configure ให้หมดอายุหลังจากช่วงเวลาที่กำหนดได้ด้วย มันได้รับการรองรับอย่างกว้างขวาง โดยมี identity provider จำนวนมากที่รองรับการสร้าง JWT และมี library จำนวนมากสำหรับใช้ JWT ภายในโค้ดของคุณเอง

Format (รูปแบบ)

Payload หลักของ JWT คือโครงสร้าง JSON ซึ่งโดยกว้างๆ แล้วสามารถมีอะไรก็ได้ตามที่คุณต้องการ เราสามารถดูตัวอย่าง token ได้ใน Example 11-1 มาตรฐาน JWT อธิบาย field ที่มีชื่อเฉพาะบางตัว (“public claim”) ที่คุณควรใช้หากมันเกี่ยวข้องกับคุณ ตัวอย่างเช่น exp กำหนดวันหมดอายุของ token ถ้าคุณใช้ public claim field เหล่านี้อย่างถูกต้อง มีโอกาสสูงที่ library ที่คุณใช้จะสามารถใช้ประโยชน์จากมันได้อย่างเหมาะสม—ปฏิเสธ token ยกตัวอย่างเช่น ถ้า field exp ระบุว่า token หมดอายุไปแล้ว 16 แม้คุณจะไม่ได้ใช้ public claim เหล่านี้ทั้งหมด ก็คุ้มค่าที่จะรู้ว่ามันคืออะไร เพื่อให้แน่ใจว่าคุณจะไม่เผลอใช้มันสำหรับวัตถุประสงค์เฉพาะของแอปพลิเคชันคุณเอง เพราะอาจทำให้ library ที่รองรับทำงานผิดปกติได้

Example 11-1. ตัวอย่างของ JSON payload ของ JWT
{
  "sub": "123",
  "name": "Sam Newman",
  "exp": 1606741736,
  "groups": "admin, beta"
}

ใน Example 11-2 เราจะเห็น token จาก Example 11-1 ที่ถูก encode แล้ว token นี้แท้จริงแล้วเป็นแค่สตริงเดียว แต่ถูกแบ่งออกเป็นสามส่วนคั่นด้วย “.”—header, payload และ signature

Example 11-2. ผลลัพธ์ของการ encode JWT payload
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. 
eyJzdWIiOiIxMjMiLCJuYW1lIjoiU2FtIE5ld21hbiIsImV4cCI6MTYwNjc0MTczNiwiZ3J... . 
Z9HMH0DGs60I0P5bVVSFixeDxJjGovQEtlNUi__iE_0

1

Header

2

Payload (ถูกตัดให้สั้นลง)

3

Signature

เพื่อประโยชน์ของตัวอย่างนี้ ผมได้แบ่งบรรทัดในแต่ละส่วน แต่ในความเป็นจริงมันจะเป็นสตริงเดียวโดยไม่มีการขึ้นบรรทัดใหม่ Header มีข้อมูลเกี่ยวกับ signing algorithm ที่ใช้ ซึ่งช่วยให้โปรแกรมที่ decode token รองรับ signing scheme ที่แตกต่างกันได้ Payload คือส่วนที่เราเก็บข้อมูลเกี่ยวกับ claim ที่ token กำลังกล่าวอ้าง—นี่คือผลลัพธ์ของการ encode โครงสร้าง JSON ใน Example 11-1 นั่นเอง Signature ใช้เพื่อให้แน่ใจว่า payload ไม่ได้ถูกปลอมแปลง และยังสามารถใช้เพื่อยืนยันว่า token ถูกสร้างขึ้นโดยใครที่คุณคิดว่าเป็น (สมมติว่า token ถูก sign ด้วย private key)

ในฐานะสตริงธรรมดา token นี้สามารถส่งผ่าน communication protocol ที่แตกต่างกันได้อย่างง่ายดาย—เป็น header ใน HTTP (ใน header Authorization ) ยกตัวอย่างเช่น หรืออาจเป็น metadata ชิ้นหนึ่งใน message สตริงที่ถูก encode นี้แน่นอนว่าสามารถถูกส่งผ่าน encrypted transport protocol ได้—เช่น TLS ผ่าน HTTP—ในกรณีนั้น token จะไม่สามารถมองเห็นได้โดยผู้ที่แอบดูการสื่อสาร

Using tokens (การใช้งาน Token)

ลองมาดูวิธีทั่วไปในการใช้ JWT token ในสถาปัตยกรรม microservice กัน ใน Figure 11-9 ลูกค้าของเราล็อกอินตามปกติ และเมื่อ authenticate แล้ว เราจะสร้าง token บางรูปแบบเพื่อแทน session ที่ล็อกอินของพวกเขา (น่าจะเป็น OAuth token) ซึ่งถูกเก็บไว้บน client device คำขอต่อๆ มาจาก client device นั้นจะไปที่ gateway ของเรา ซึ่งจะสร้าง JWT token ที่จะใช้ได้เฉพาะสำหรับระยะเวลาของคำขอนั้น JWT token นี้คือสิ่งที่ถูกส่งไปยัง downstream microservice ต่อไป พวกมันสามารถ validate token และดึง claim จาก payload เพื่อตัดสินว่า authorization แบบไหนถึงจะเหมาะสม

bms2 1109

Figure 11-9. JWT token ถูกสร้างขึ้นสำหรับคำขอที่เจาะจง และถูกส่งไปยัง downstream microservice

อีกรูปแบบหนึ่งของแนวทางนี้คือการสร้าง JWT token เมื่อผู้ใช้ authenticate ตัวเองครั้งแรกกับระบบ แล้วเก็บ JWT token นั้นไว้บน client device ถึงกระนั้นก็คุ้มค่าที่จะพิจารณาว่า token แบบนี้จะต้องใช้ได้ตลอดระยะเวลาของ session ที่ล็อกอินอยู่ อย่างที่เราได้พูดถึงไปแล้ว เราอยากจำกัดระยะเวลาความถูกต้องของ credential ที่ระบบสร้างขึ้น เพื่อลดโอกาสที่มันจะถูกใช้ในทางที่ผิด และเพื่อลดผลกระทบหากเราจำเป็นต้องเปลี่ยน key ที่ใช้สร้าง encoded token การสร้าง JWT token แบบต่อคำขอดูเหมือนจะเป็นทางออกที่พบบ่อยที่สุดสำหรับปัญหานี้ ดังที่เราแสดงใน Figure 11-9 การมี token exchange บางรูปแบบทำในตัว gateway ก็สามารถทำให้ง่ายขึ้นมากในการนำ JWT token มาใช้ โดยไม่ต้องเปลี่ยนแปลงส่วนใดของ authentication flow ที่เกี่ยวข้องกับการสื่อสารกับ client device—ถ้าคุณมีโซลูชัน SSO ที่ทำงานได้อยู่แล้ว การซ่อนข้อเท็จจริงที่ว่า JWT token ถูกใช้อยู่จาก main user authentication flow จะทำให้การเปลี่ยนแปลงแบบนี้รบกวนน้อยลง

ดังนั้นด้วยการสร้าง JWT token ที่เหมาะสม downstream microservice ของเราจะสามารถได้รับข้อมูลทั้งหมดที่จำเป็นเพื่อยืนยันตัวตนของผู้ใช้ที่ส่งคำขอ รวมถึงข้อมูลเพิ่มเติมเช่นกลุ่มหรือ role ที่ผู้ใช้นั้นอยู่ ความถูกต้องของ token นี้ก็สามารถถูกตรวจสอบได้โดย microservice ด้วยการตรวจสอบ signature ของ JWT token เช่นกัน เมื่อเทียบกับทางออกก่อนหน้านี้ในพื้นที่นี้ (เช่น nested SAML assertion) JWT token ทำให้กระบวนการกระจายศูนย์ authorization ในสถาปัตยกรรม microservice ง่ายขึ้นมาก

Challenges (ความท้าทาย)

มีประเด็นบางอย่างเกี่ยวกับ JWT token ที่คุ้มค่าที่จะจดจำไว้ อย่างแรกคือประเด็นเรื่อง key ในกรณีของ signed JWT token เพื่อยืนยัน signature ผู้รับ JWT token จะต้องมีข้อมูลบางอย่างที่ต้องสื่อสารนอกช่องทางปกติ (out of band)—โดยปกติคือ public key ประเด็นเรื่องการจัดการ key ทั้งหมดจะนำมาใช้ในกรณีนี้ microservice จะได้ public key มาได้อย่างไร? จะเกิดอะไรขึ้นถ้า public key ต้องเปลี่ยน? Vault เป็นตัวอย่างของเครื่องมือที่ microservice สามารถใช้เพื่อดึง (และจัดการ rotation ของ) public key ได้ และมันถูกออกแบบมาให้ทำงานในสภาพแวดล้อมที่กระจายศูนย์สูงอยู่แล้ว แน่นอนว่าคุณสามารถแค่ hardcode public key ไว้ในไฟล์ configuration สำหรับ microservice ที่รับข้อมูลได้ แต่คุณก็จะมีปัญหาต้องจัดการเมื่อ public key เปลี่ยนแปลง

ประการที่สอง การกำหนดเวลาหมดอายุให้ถูกต้องสำหรับ token อาจเป็นเรื่องยากหากมีเวลาประมวลผลที่ยาวนานเข้ามาเกี่ยวข้อง ลองนึกถึงสถานการณ์ที่ลูกค้าวางคำสั่งซื้อ สิ่งนี้จะเริ่มชุดของกระบวนการแบบ asynchronous ที่อาจใช้เวลาหลายชั่วโมงหรือหลายวันกว่าจะเสร็จสมบูรณ์ โดยไม่มีการเกี่ยวข้องเพิ่มเติมจากลูกค้า (รับชำระเงิน ส่งอีเมลแจ้งเตือน ให้สินค้าถูกแพ็คและจัดส่ง ฯลฯ) ดังนั้นคุณจำเป็นต้องสร้าง token ที่มีระยะเวลาความถูกต้องตรงกันหรือไม่? คำถามตรงนี้คือ ณ จุดไหนที่การมี token ที่มีอายุยาวนานกว่าเริ่มกลายเป็นปัญหามากกว่าการไม่มี token เลย? ผมเคยคุยกับทีมหลายทีมที่จัดการกับปัญหานี้ บางทีมสร้าง token แบบมีอายุยาวเป็นพิเศษที่ถูกจำกัดขอบเขตให้ใช้ได้เฉพาะในบริบทนี้เท่านั้น ทีมอื่นๆ ก็แค่หยุดใช้ token ณ จุดหนึ่งใน flow ผมยังไม่ได้ดูตัวอย่างของปัญหานี้มากพอที่จะบอกได้ว่าทางออกที่ถูกต้องคืออะไร แต่มันเป็นประเด็นที่ควรตระหนักไว้

สุดท้าย ในบางสถานการณ์คุณอาจต้องการข้อมูลใน JWT token มากจนตัว token เองมีขนาดใหญ่จนกลายเป็นปัญหา แม้สถานการณ์แบบนี้จะพบได้ไม่บ่อย แต่มันก็เกิดขึ้นจริง เมื่อหลายปีก่อน ผมเคยคุยกับทีมหนึ่งเกี่ยวกับการใช้ token เพื่อจัดการ authorization สำหรับส่วนหนึ่งของระบบที่จัดการ rights management สำหรับเพลง ตรรกะรอบเรื่องนี้ซับซ้อนมาก—ลูกค้าของผมคำนวณได้ว่าสำหรับแต่ละ track มันอาจต้องการ entry มากถึง 10,000 รายการใน token เพื่อจัดการกับสถานการณ์ต่างๆ ที่แตกต่างกัน เราพบว่าอย่างน้อยในโดเมนนั้น มีเพียง use case ที่เจาะจงหนึ่งเดียวที่ต้องการข้อมูลจำนวนมากขนาดนี้ ในขณะที่ระบบส่วนใหญ่สามารถทำงานได้ด้วย token ที่มี field น้อยกว่า ในสถานการณ์แบบนี้ มันสมเหตุสมผลที่จะจัดการกับกระบวนการ authorization การจัดการ rights ที่ซับซ้อนกว่านี้ด้วยวิธีอื่น—โดยพื้นฐานแล้วใช้ JWT token สำหรับ authorization แบบ “ง่าย” เบื้องต้น แล้วทำการ lookup เพิ่มเติมกับ data store เพื่อดึง field เพิ่มเติมตามที่จำเป็น สิ่งนี้ทำให้ระบบส่วนใหญ่สามารถทำงานได้ด้วย token เพียงอย่างเดียว