怎样用六步实现一个安全的中心化认证系统?

随着Shopify商家规模的不断扩大,他们会在自己的组织中引入多家商店。以前,这需要请员工到每家商店设置他们的账户。不过,这样让员工管理起来就更麻烦,要做的事情变多,因为他们要管理很多账户。

为解决上述问题,我们创建了一项新服务来处理中心化身份验证和用户身份管理事务,服务的名字就叫身份(Identity)。我们在OpenID Connect(OIDC)规范的基础上,在Shopify构建一套中心化身份验证服务。

有了这套系统,我们就有了一个解决方案,能允许用户可靠、安全地合并其帐户,从而实现单点登录并从中获益。解决这一问题的团队由产品管理、用户体验、工程和数据科学领域的专家组成,分布在三个城市(渥太华、蒙特利尔和滑铁卢)的团队成员共同协作。

店铺模型

在Shopify的构建设计中,属于特定商店(在我们的数据模型中称为Shop,店铺)的所有数据都会驻留在单个数据库实例中。这里的数据包括产品、订单、客户和用户等核心业务对象。用户模型代表的是有权访问特定商店的管理界面,并具有特定权限的员工。

商店商务对象关系

用户身份验证和配置文件管理属于店铺本身,并且只要你在Shopify上的足迹没有走出单个商店,它就一直生效。商家的组织扩展到多个商店后,管理商店用户的人员和这些用户都会面临更多事务。

由于各个店铺间不会共享数据,所以就没有跨商店的单点登录(SSO)功能。换句话说,你必须单独登录每个商店。用户必须针对他们有权访问的每个商店都维护一份个人资料数据、密码和两步身份验证信息。

商店与用户隔离

在身份服务内建立用户账户模型

在我们的身份服务中,建模的用户帐户有两大类型:分别是身份(Identity)帐户和旧(Legacy)帐户。用户可通过OIDC访问的服务或应用程序被建模为身份账户中的目的地(Destination)。Shopify中目的地包括了商店、合作伙伴仪表板或我们的社区讨论论坛等。

旧帐户只能访问一个商店,而身份帐户可以用来访问多个目的地。

旧帐户模型:每个帐户一个目的地。只能进入店铺

我们的设计中,新帐户都会创建为身份帐户,且拥有旧帐户的已有用户可以安全地升级到身份帐户。最大的问题是将多个旧帐户合并在一起。当用户使用同一个电子邮件地址登录多个不同的Shopify商店时,我们会将这些帐户合并为一个身份帐户,而不会阻止他们访问所使用的任何商店。

组合帐户模型:每个帐户可以访问多个目的地

要做到一个账户行天下,我们有六步要做。

  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对象。

用户拥有多个可以合并的帐户时看到的提示

Shopify不需要用户在创建新商店时验证他们的电子邮件地址。这意味着有人可能会使用他们无权访问的电子邮件地址来注册试用。因此,我们需要先验证你对电子邮件地址拥有访问权限,然后才能显示与相同电子邮件关联的其他帐户的用户信息,或者允许你对其他帐户执行任何操作。这一验证过程中,你需要请求将带有链接的电子邮件发送到你的地址。

如果用户登录商店使用的电子邮件地址通过了验证,我们将列出使用该电子邮件地址的所有其他目的地。如果用户要登录账户的电子邮件地址未通过验证,那么我们只会指出该地址还关联了其他帐户的事实,并且在继续合并过程之前,用户必须验证自己的电子邮件地址。

用户使用未经验证的电子邮件地址登录时看到的提示

如果需要合并的所有帐户都开启了2FA,则用户必须为每个要合并的帐户提供一个有效代码。当用户使用SMS作为2FA方法时,如果他们在多个帐户中使用相同的电话号码,则可能会在此步骤中节省一些时间——因为对于使用相同号码的所有目的地,我们只需要一个代码即可。

对我们的用户来说,这样的便利更安全,目的是要减少在这一步骤上花费的时间。但是,使用身份验证器应用(例如谷歌身份验证、Authy、1Password等)的用户必须为每个目的地都提供代码,因为身份验证应用是针对各个用户帐户配置的,彼此之间没有任何关联。

如果用户除了自己已经登录的帐户外,无法为其他某个帐户提供2FA代码,则可以将这个帐户排除在合并范围之外。某人可能无法提供代码的正当理由包括:该帐户使用了他无法使用的旧SMS电话号码,或者他不再拥有配置为该帐户生成代码的身份验证应用。

这里的思想是,将来用户重新获得对某个帐户的访问权限时,可以在那时候再合并之前被排除的帐户。

满足所有帐户的2FA要求后,我们会提示用户为其合并帐户设置一个新密码。我们将加密的密码哈希存储在跟踪这个会话状态的对象上。

四.提示用户启用双因素认证(2FA)来保护自己的帐户

让用户参与帐户的维护工作是一个极好的机会,可以借此让他们从双因素帐户保护措施中受益。

我们向已经在至少一个帐户中启用了2FA的用户显示的是另一种流程,因为这里的假设是不需要向他们解释什么是2FA,但是从未设置过2FA的用户很有可能需要这种解释。

五.创建组合身份帐户

一旦用户确认了自己选择的2FA配置,或选择不对其进行设置,我们将执行以下操作:

将2FA设置(如果存在)附加到跟踪特定帐户组合会话(MergeSession)的对象。

合并具有新密码和2FA配置的会话对象

在单个数据库事务中,创建一个完整的新帐户,将旧帐户中的目的地与该帐户关联,并删除旧帐户。

从用户那里获取所有信息后,我们需要在一个事务内执行这步操作,以免削弱帐户的安全性。如果用户在开始此过程前就在使用2FA,并且我们在获得新密码后立即创建了身份帐户,则会存在一个很短的时间范围,其中新身份帐户的安全性会低于旧帐户。

身份帐户一旦存在并具有与之关联的密码,就可以在只知道密码的情况下将其用于访问目的地。将帐户创建推迟到密码和2FA都定义好之后的话,新帐户就能像合并之前的帐户一样安全。

合并账户的最终状态

为新帐户生成一个会话,并使用它来满足最初发起该会话的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

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/u8LOZUhbTZfEE6h1clLI
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券