Categories
程式開發

怎樣用六步實現一個安全的中心化認證系統?


怎樣用六步實現一個安全的中心化認證系統? 1

隨著Shopify商家規模的不斷擴大,他們會在自己的組織中引入多家商店。以前,這需要請員工到每家商店設置他們的賬戶。不過,這樣讓員工管理起來就更麻煩,要做的事情變多,因為他們要管理很多賬戶。

為解決上述問題,我們創建了一項新服務來處理中心化身份驗證和用戶身份管理事務,服務的名字就叫身份(Identity)。我們在OpenID Connect(OIDC)規範的基礎上,在Shopify構建一套中心化身份驗證服務。

有了這套系統,我們就有了一個解決方案,能允許用戶可靠、安全地合併其帳戶,從而實現單點登錄並從中獲益。解決這一問題的團隊由產品管理、用戶體驗、工程和數據科學領域的專家組成,分佈在三個城市(渥太華、蒙特利爾和滑鐵盧)的團隊成員共同協作。

店鋪模型

在Shopify的構建設計中,屬於特定商店(在我們的數據模型中稱為Shop,店鋪)的所有數據都會駐留在單個數據庫實例中。這裡的數據包括產品、訂單、客戶和用戶等核心業務對象。用戶模型代表的是有權訪問特定商店的管理界面,並具有特定權限的員工。

怎樣用六步實現一個安全的中心化認證系統? 2

商店商務對象關係

用戶身份驗證和配置文件管理屬於店鋪本身,並且只要你在Shopify上的足跡沒有走出單個商店,它就一直生效。商家的組織擴展到多個商店後,管理商店用戶的人員和這些用戶都會面臨更多事務。

由於各個店鋪間不會共享數據,所以就沒有跨商店的單點登錄(SSO)功能。換句話說,你必須單獨登錄每個商店。用戶必須針對他們有權訪問的每個商店都維護一份個人資料數據、密碼和兩步身份驗證信息。

怎樣用六步實現一個安全的中心化認證系統? 3

商店與用戶隔離

在身份服務內建立用戶賬戶模型

在我們的身份服務中,建模的用戶帳戶有兩大類型:分別是身份(Identity)帳戶和舊(Legacy)帳戶。用戶可通過OIDC訪問的服務或應用程序被建模為身份賬戶中的目的地(Destination)。 Shopify中目的地包括了商店、合作夥伴儀表板或我們的社區討論論壇等。

舊帳戶只能訪問一個商店,而身份帳戶可以用來訪問多個目的地。

怎樣用六步實現一個安全的中心化認證系統? 4

舊帳戶模型:每個帳戶一個目的地。只能進入店鋪

我們的設計中,新帳戶都會創建為身份帳戶,且擁有舊帳戶的已有用戶可以安全地升級到身份帳戶。最大的問題是將多個舊帳戶合併在一起。當用戶使用同一個電子郵件地址登錄多個不同的Shopify商店時,我們會將這些帳戶合併為一個身份帳戶,而不會阻止他們訪問所使用的任何商店。

怎樣用六步實現一個安全的中心化認證系統? 5

組合帳戶模型:每個帳戶可以訪問多個目的地

要做到一個賬戶行天下,我們有六步要做。

  1. 將現有用戶帳戶中的數據同步到中心化身份服務中

  2. 通過OpenID Connect使所有身份驗證都通過中心化身份服務處理

  3. 提示用戶將他們的帳戶合併在一起

  4. 提示用戶啟用雙因素認證(2FA)來保護他們的帳戶

  5. 創建組合的身份帳戶

  6. 避免創建新的舊帳戶

一.將數據從現有用戶賬戶中同步到中心化身份服務中

我們會確保所有用戶配置文件和安全憑證信息都從他們管理的商店同步到中心化的身份服務中。這意味著每次以下用戶事件之一發生時,就會將數據從存儲區同步到身份服務上

  • 創建
  • 刪除
  • 個人資料數據更新
  • 安全數據更新(密碼或2FA)

二.通過OpenID Connect(OIDC)使所有身份驗證都通過中心化身份服務處理

OpenID Connect是對OpenID 2.0規範的擴展,並且是用來將認證從商店委託給身份服務的方法。在執行此步驟前,所有密碼和2FA驗證均在核心店鋪應用程序運行時內完成。

鑑於Shopify對用於核心平台的數據庫是按店鋪分片的,所以與特定店鋪相關的所有數據都在單個數據庫實例上可用。

讓所有身份驗證都通過身份服務處理的一個缺點是,當用戶首次登錄一個Shopify服務時,它要求將用戶的瀏覽器數據發送到身份服務,以執行OIDC身份驗證請求(AuthRequest),因此初始登錄到一個商店時延遲會長一些。

三.提示用戶將他們的帳戶合併在一起

如果用戶的電子郵件地址可以登錄多個Shopify服務,我們會提示他們將帳戶合併到一個身份帳戶中。當舊用戶登錄到一個Shopify產品時,我們在驗證了他們身份,但將他們發送到目的地之前中斷OIDC的AuthRequest流程,以檢查他們是否擁有可以升級的帳戶。

用戶有兩個主要的身份帳戶升級途徑:自動升級單個舊帳戶或合併多個帳戶。

當用戶的電子郵件地址只有一個關聯商店時,就會自動升級一個舊帳戶。在這種情況下,我們會將單個帳戶轉換為身份賬戶,保留其所有配置文件、密碼和2FA設置。身份服務中的帳戶使用單表繼承來建模,該繼承具有一個類型屬性,用來指定特定記錄使用哪些類。

在這種情況下,升級舊帳戶時只要更新這個類型屬性的值即可。這不需要在Shopify系統中的其他任何位置進行其他任何更改,因為該帳戶的通用唯一標識符(UUID)是不變的,這是用來在其他系統中標識某個帳戶的值。

當用戶的同一電子郵件地址對應多個活動帳戶(舊賬戶或身份賬戶)時,將觸發多個帳戶的合併操作。我們為這種合併過程創建了一個新的會話對象,稱為MergeSession,用來跟踪創建身份帳戶所需的所有數據。 MergeSession與一個單獨的AuthRequest相關聯,這意味著當AuthRequest完成時,該會話將不再處於活動狀態。如果用戶經歷了多個合併過程,則我們必須為每個合併過程生成一個新的MergeSession對象。

怎樣用六步實現一個安全的中心化認證系統? 6

用戶擁有多個可以合併的帳戶時看到的提示

Shopify不需要用戶在創建新商店時驗證他們的電子郵件地址。這意味著有人可能會使用他們無權訪問的電子郵件地址來註冊試用。因此,我們需要先驗證你對電子郵件地址擁有訪問權限,然後才能顯示與相同電子郵件關聯的其他帳戶的用戶信息,或者允許你對其他帳戶執行任何操作。這一驗證過程中,你需要請求將帶有鏈接的電子郵件發送到你的地址。

如果用戶登錄商店使用的電子郵件地址通過了驗證,我們將列出使用該電子郵件地址的所有其他目的地。如果用戶要登錄賬戶的電子郵件地址未通過驗證,那麼我們只會指出該地址還關聯了其他帳戶的事實,並且在繼續合併過程之前,用戶必須驗證自己的電子郵件地址。

怎樣用六步實現一個安全的中心化認證系統? 7

用戶使用未經驗證的電子郵件地址登錄時看到的提示

如果需要合併的所有帳戶都開啟了2FA,則用戶必須為每個要合併的帳戶提供一個有效代碼。當用戶使用SMS作為2FA方法時,如果他們在多個帳戶中使用相同的電話號碼,則可能會在此步驟中節省一些時間——因為對於使用相同號碼的所有目的地,我們只需要一個代碼即可。

對我們的用戶來說,這樣的便利更安全,目的是要減少在這一步驟上花費的時間。但是,使用身份驗證器應用(例如穀歌身份驗證、Authy、1Password等)的用戶必須為每個目的地都提供代碼,因為身份驗證應用是針對各個用戶帳戶配置的,彼此之間沒有任何關聯。

如果用戶除了自己已經登錄的帳戶外,無法為其他某個帳戶提供2FA代碼,則可以將這個帳戶排除在合併範圍之外。某人可能無法提供代碼的正當理由包括:該帳戶使用了他無法使用的舊SMS電話號碼,或者他不再擁有配置為該帳戶生成代碼的身份驗證應用。

這裡的思想是,將來用戶重新獲得對某個帳戶的訪問權限時,可以在那時候再合併之前被排除的帳戶。

滿足所有帳戶的2FA要求後,我們會提示用戶為其合併帳戶設置一個新密碼。我們將加密的密碼哈希存儲在跟踪這個會話狀態的對像上。

四.提示用戶啟用雙因素認證(2FA)來保護自己的帳戶

讓用戶參與帳戶的維護工作是一個極好的機會,可以藉此讓他們從雙因素帳戶保護措施中受益。

我們向已經在至少一個帳戶中啟用了2FA的用戶顯示的是另一種流程,因為這裡的假設是不需要向他們解釋什麼是2FA,但是從未設置過2FA的用戶很有可能需要這種解釋。

五.創建組合身份帳戶

一旦用戶確認了自己選擇的2FA配置,或選擇不對其進行設置,我們將執行以下操作:

將2FA設置(如果存在)附加到跟踪特定帳戶組合會話(MergeSession)的對象。

怎樣用六步實現一個安全的中心化認證系統? 8

合併具有新密碼和2FA配置的會話對象

在單個數據庫事務中,創建一個完整的新帳戶,將舊帳戶中的目的地與該帳戶關聯,並刪除舊帳戶。

從用戶那裡獲取所有信息後,我們需要在一個事務內執行這步操作,以免削弱帳戶的安全性。如果用戶在開始此過程前就在使用2FA,並且我們在獲得新密碼後立即創建了身份帳戶,則會存在一個很短的時間範圍,其中新身份帳戶的安全性會低於舊帳戶。

身份帳戶一旦存在並具有與之關聯的密碼,就可以在只知道密碼的情況下將其用於訪問目的地。將帳戶創建推遲到密碼和2FA都定義好之後的話,新帳戶就能像合併之前的帳戶一樣安全。

怎樣用六步實現一個安全的中心化認證系統? 9

合併賬戶的最終狀態

為新帳戶生成一個會話,並使用它來滿足最初發起該會話的AuthRequest。

此過程中,一些較複雜的邏輯部分包括:查找給定電子郵件地址的所有相關帳戶以及他們有權訪問的目的地的信息,在創建身份帳戶時替換舊帳戶,並確保身份帳戶設置時所有必要數據都正確定義好。

針對方案中的這些需求,我們依賴一個名為ActiveOperation的Ruby庫。這是一個非常小的框架,允許你在一個操作類中隔離和建模應用程序中的業務邏輯。傳統上,在Rails應用程序中,邏輯最後必須放入控制器或模型中;在我們的情況下,我們將復雜的業務邏輯定義為操作,從而獲得了非常小的控制器和模型。鑑於這些操作是隔離的,並且每個單獨的類都要負責非常具體的職責,因此這些操作很容易測試。

還有其他庫可以處理這種業務邏輯流程,但是我們選擇ActiveOperation的原因是它易於使用,讓我們的代碼更容易理解,並且對我們使用的RSpec測試框架具有內置支持。

在開始向用戶推出帳戶合併流程時,我們在身份服務中添加了對新的Web身份驗證(WebAuthn)標準的支持。這意味著我們能允許用戶在保護帳戶安全時使用物理安全密鑰作為第二個因素,在原有的SMS或身份驗證應用的基礎上又多了一個選擇。

六.防止創建新的舊帳戶

我們不想再創建任何舊帳戶了。使用身份創建流程需要更新兩個用戶場景:在shopify.com上註冊新的試用商店,以及邀請新員工進入現有商店。

註冊新商店時,你將在這一過程中輸入你的電子郵件地址。該電子郵件地址會被用作新商店的主要所有者。對於舊帳戶,即使電子郵件地址屬於其他商店,我們仍將為新創建的商店創建新的舊帳戶。

邀請新員工到你的商店時,你將輸入新用戶的電子郵件地址,然後將向該電子郵件地址發送邀請郵件,其中包括接受邀請並完成帳戶設置的鏈接。與商店創建過程類似,這會在每個單獨的商店上創建一個新的舊賬戶。

在這兩種情況下,我們都將引入新的流程來確定這個電子郵件地址是否已經屬於一個身份帳戶;如果是,則要求用戶先驗證屬於該電子郵件地址的帳戶,然後才能繼續。

總結

截至本文撰寫時,我們超過75%的活躍用戶帳戶已自動升級或合併到一個身份帳戶中了。不需要用戶交互的帳戶(例如可以自動升級的帳戶)可以自動完成升級,而無需用戶登錄。要求用戶證明其帳戶所有權的帳戶只能在登錄時完成合併。將來,我們將阻止用戶在沒有身份帳戶的情況下登錄Shopify。

當Shopify的產品團隊面對的都是擁有身份帳戶的活躍用戶時,我們就可以開始為那些將身份驗證和配置文件管理委派給身份服務的用戶建立新體驗。驗證過程還是要由利用這些身份賬戶的服務來處理,因為身份服務專門是用來處理身份驗證的,而對有關帳戶可以訪問的服務權限一無所知。

對我們的用戶來說,這意味著當Shopify啟動使用身份服務處理用戶登錄的新服務時,他們就不必再創建和管理新帳戶了。

英文原文:

How to Implement a Secure Central Authentication Service in Six Steps