如何使用 DiscourseConnect 实现单点登录?

DiscourseConnect 是 Discourse 的一项核心功能,允许您配置“单点登录 (SSO)”,将 Discourse 的所有用户注册和登录完全外包给另一个站点。此功能提供给我们的标准、商业和企业托管客户

:information_source: (2021 年 2 月)“Discourse SSO”现已更名为“DiscourseConnect”。如果您运行的是旧版本的 Discourse,则以下设置将被命名为 sso_... 而不是 discourse_connect_...

问题所在

许多希望与 Discourse 站点集成的站点都希望将所有用户注册保留在一个单独的站点中。在这种设置中,所有登录操作都应外包给该不同的站点。

如果我希望将 SSO 与现有身份验证结合使用,该怎么办?

DiscourseConnect 的目的是取代 Discourse 身份验证,如果您想添加新的提供程序,请参阅现有的插件,例如:Discourse VK 身份验证 (vkontakte)

启用 DiscourseConnect

要启用 DiscourseConnect,您需要填写 3 个设置:

Screenshot 2021-02-11 at 12.40.34

enable_discourse_connect:必须启用,全局开关
discourse_connect_url:用户尝试登录时将被发送到的站外 URL
discourse_connect_secret:用于对 SSO 有效负载进行哈希处理的秘密字符串。确保有效负载的真实性。

一旦 enable_discourse_connect 设置为 true:

  • 单击登录或头像,将您重定向到 /session/sso,然后将用户重定向到带有签名有效负载的 discourse_connect_url
  • 不允许用户“更改密码”。该字段已从用户配置文件中删除。
  • 用户将无法再使用 Discourse 身份验证(用户名/密码、谷歌等)

如果您不小心选中了它怎么办?

请参阅:使用只读模式或无效的 SSO 配置将自己锁定后,以管理员身份重新登录

在您的站点上实现 DiscourseConnect

:warning: Discourse 使用电子邮件将外部用户映射到 Discourse 用户,并假定外部电子邮件是安全的。如果您在将电子邮件地址发送到 DISCOURSE 之前未对其进行验证,您的站点将非常容易受到攻击!

或者,如果您坚持发送未经验证的电子邮件,请务必设置 require_activation=true,这将强制 Discourse 验证所有电子邮件。我们仍然强烈建议您不要这样做,因此如果您继续启用该设置,您将承担巨大的风险。

Discourse 会将客户端重定向到带有签名有效负载的 discourse_connect_url:(假设 discourse_connect_urlhttps://somesite.com/sso

您将收到以下传入流量

https://somesite.com/sso?sso=PAYLOAD&sig=SIG

有效负载是一个 Base64 编码的字符串,由一个 nonce 和一个 return_sso_url 组成。有效负载始终是一个有效的查询字符串。

例如,如果 nonce 是 ABCD。raw_payload 将是:

nonce=ABCD&return_sso_url=https%3A%2F%2Fdiscourse_site%2Fsession%2Fsso_login,这个原始有效负载是 base 64 编码的。

被调用的端点必须

  1. 验证签名:确保 PAYLOAD 的 HMAC-SHA256(使用 discourse_connect_secret 作为密钥)等于 sigsig 将进行十六进制编码)。
  2. 执行它必须执行的任何身份验证
  3. 创建一个至少包含 nonceemailexternal_id 的新的 URL 编码有效负载。您还可以提供一些附加数据,以下是 Discourse 可以理解的所有密钥的列表:
    • nonce 应从输入有效负载中复制
    • email 必须是经过验证的电子邮件地址。如果电子邮件地址未经验证,请将 require_activation 设置为“true”。
    • external_id 是用户唯一的任何字符串,即使他们的电子邮件、姓名等发生更改,该字符串也不会更改。建议的值是您数据库的“id”行号。
    • 如果用户是新用户或设置了 SiteSetting.auth_overrides_usernameusername 将成为 Discourse 上的用户名。
    • 如果用户是新用户或设置了 SiteSetting.auth_overrides_namename 将成为 Discourse 上的全名。
    • 如果用户是新用户或设置了 SiteSetting.discourse_connect_overrides_avataravatar_url 将被下载并设置为用户的头像。
    • avatar_force_update 是一个布尔字段。如果设置为 true,它将强制 Discourse 更新用户的头像,无论 avatar_url 是否已更改。
    • 如果用户是新用户、他们的个人简介为空或设置了 SiteSetting.discourse_connect_overrides_biobio 将成为用户个人简介的内容。
    • 其他布尔字段(“true”或“false”)包括:adminmoderatorsuppress_welcome_message
  4. Base64 编码有效负载
  5. 使用 discourse_connect_secret 作为密钥,使用 Base64 编码的有效负载作为文本,计算有效负载的 HMAC-SHA256 哈希
  6. 使用 ssosig 查询参数重定向回 return_sso_url (http://discourse_site/session/sso_login?sso=payload&sig=sig)

Discourse 将验证 nonce 是否有效,如果有效,它将立即过期,使其无法再次使用。然后,它将尝试:

  1. 通过在 SingleSignOnRecord 模型中查找已关联的 external_id 来登录用户
  2. 使用提供的电子邮件登录用户(更新 external_id)(除非 require_activation = true
  3. 为用户提供(电子邮件、用户名、姓名)更新 external_id 创建一个新帐户

安全问题

nonce(一次性令牌)将在 10 分钟后自动过期。这意味着一旦用户被重定向到您的站点,他们有 10 分钟的时间登录/创建新帐户。

该协议可以安全地防止重放攻击,因为 nonce 只能使用一次。nonce 与当前浏览器会话绑定,以防止 CSRF 攻击。

指定组成员资格

如果指定了 discourse connect 覆盖组 选项,Discourse 将考虑在 groups 中传递的以逗号分隔的组列表。

Screenshot 2021-02-11 at 12.35.15

除了 groups 之外,您还可以使用 add_groupsremove_groups 属性在 SSO 有效负载中指定组成员资格,而不管 discourse connect 覆盖组 选项如何。

add_groups 是一个逗号分隔的组名列表,我们将确保用户是其成员。
remove_groups 是一个逗号分隔的组名列表,我们将确保用户不是其成员。

参考实现

Discourse 包含 SSO 类的参考实现:

discourse/lib/discourse_connect_base.rb at main · discourse/discourse · GitHub

一个简单的实现将是:

class DiscourseSsoController < ApplicationController
  def sso
    secret = "MY_SECRET_STRING"
    sso = DiscourseApi::SingleSignOn.parse(request.query_string, secret)
    sso.email = "[email protected]"
    sso.name = "Bill Hicks"
    sso.username = "[email protected]"
    sso.external_id = "123" # 您的应用程序的每个用户的唯一 ID
    sso.sso_secret = secret

    redirect_to sso.to_url("http://l.discourse/session/sso_login")
  end
end

转换到和从单点登录。

只要请求有效负载中未将 require_activation 参数设置为 true,系统就会信任单点登录端点提供的电子邮件。这意味着如果您过去在 Discourse 上禁用了 DiscourseConnect 的现有帐户,DiscourseConnect 将简单地重新使用它并避免创建新帐户。

如果您关闭 DiscourseConnect,用户将能够重置密码并重新访问他们的帐户。

真实世界的例子:

给定以下设置:

Discourse 域名:http://discuss.example.com
DiscourseConnect url:http://www.example.com/discourse/sso
DiscourseConnect 密钥:d836444a9e4084d5b224a60c208dce14
电子邮件已验证:否(将 require_activation=true 添加到有效负载)

用户尝试登录

  • 生成 Nonce:cb68251eefb5211e58c00ff1395f0c0b
  • 生成原始有效负载:nonce=cb68251eefb5211e58c00ff1395f0c0b
  • 有效负载是 Base64 编码的:bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGI=
  • 有效负载是 URL 编码的:bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGI%3D
  • 在 Base64 编码的有效负载上生成 HMAC-SHA256:1ce1494f94484b6f6a092be9b15ccc1cdafb1f8460a3838fbb0e0883c4390471

最后,浏览器被重定向到:

http://www.example.com/discourse/sso?sso=bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGI%3D&sig=1ce1494f94484b6f6a092be9b15ccc1cdafb1f8460a3838fbb0e0883c4390471

在另一端

  1. 使用 HMAC-SHA256 验证 有效负载,如果 sig 不匹配,则进程中止。
  2. 通过反转上述步骤提取 nonce。

用户登录:

name: sam
external_id: hello123
email: [email protected]
username: samsam
require_activation: true

生成未签名的有效负载:

nonce=cb68251eefb5211e58c00ff1395f0c0b&name=sam&username=samsam&email=test%40test.com&external_id=hello123&require_activation=true

顺序无关紧要,值是 URL 编码的

有效负载是 Base64 编码的

bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT1zYW0mdXNlcm5hbWU9c2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRlcm5hbF9pZD1oZWxsbzEyMyZyZXF1aXJlX2FjdGl2YXRpb249dHJ1ZQ==

有效负载是 URL 编码的

bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT1zYW0mdXNlcm5hbWU9c2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRlcm5hbF9pZD1oZWxsbzEyMyZyZXF1aXJlX2FjdGl2YXRpb249dHJ1ZQ%3D%3D

Base64 编码的有效负载已签名

3d7e5ac755a87ae3ccf90272644ed2207984db03cf020377c8b92ff51be3abc3

浏览器重定向到:

http://discuss.example.com/session/sso_login?sso=bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT1zYW0mdXNlcm5hbWU9c2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRlcm5hbF9pZD1oZWxsbzEyMyZyZXF1aXJlX2FjdGl2YXRpb249dHJ1ZQ%3D%3D&sig=3d7e5ac755a87ae3ccf90272644ed2207984db03cf020377c8b92ff51be3abc3

同步 DiscourseConnect 记录

您可以使用 POST 管理员端点 /admin/users/sync_sso 来同步 DiscourseConnect 记录,将您将传递给 DiscourseConnect 端点的相同记录传递给它,nonce 无关紧要。

如果您从另一个站点调用 admin/users/sync_sso,则需要在请求的标头中包含有效的管理员 api_key 和有效的 api_username。有关如何构造请求的更多详细信息,请参阅使用 sync_sso 路由同步 DiscourseConnect 用户数据

清除 DiscourseConnect 记录

如果您的 DiscourseConnect 提供程序的 external_id 值已更改(也许您更改了生成算法,也许它是不同的端点),您可以使用 rails 控制台安全地删除所有现有记录:

SingleSignOnRecord.destroy_all

注销用户

如果需要,您可以使用 POST 管理员端点 /admin/users/{USER_ID}/log_out 注销系统中的任何用户。

要配置端点 Discourse 在注销时重定向到,请搜索 logout redirect 设置。如果此处未设置 URL,您将被重定向回 discourse connect url 中配置的 URL。

external_id 搜索用户

可以使用 /users/by-external/{EXTERNAL_ID}.json 端点访问用户配置文件数据。这将返回一个包含用户信息的 JSON 有效负载,包括 user_id,它可以与 log_out 端点一起使用。

现有实现

  • discourse_api gem 可用于 SSO。查看其示例目录中的 SSO 代码以查看基本实现。
  • 我们的 WordPress 插件 可以轻松配置 WordPress 和 Discourse 之间的 SSO。有关设置它的详细信息,请参见插件选项页面的 SSO 选项卡。

未来的工作

  • 我们希望收集更多其他平台上 SSO 的参考实现。如果您有,请发布到 Dev / SSO 类别

高级功能

  • 您可以通过在字段名称前加上 custom 前缀来传递自定义用户字段。例如,custom.user_field_1 可用于设置名称为 user_field_1UserCustomField 的值。
  • 您可以传递 avatar_url 来覆盖用户头像(需要启用 SiteSetting.discourse_connect_overrides_avatar)。头像会被缓存,因此传递 avatar_force_update=true 以强制它们在 url 相同时更新。现在,您不能传递空 url 来禁用用户的头像。
  • 默认情况下,欢迎消息将发送给通过 SSO 创建的所有新用户。如果您希望禁止显示此消息,您可以传递 suppress_welcome_message=true
  • 要将您的 Discourse 实例配置为 Discourse connect 提供程序,请参阅:使用 DiscourseConnect 作为身份提供程序

调试您的 DiscourseConnect 提供程序

为了协助调试 DiscourseConnect,您可以启用站点设置 verbose_discourse_connect_logging。通过启用该站点设置,丰富的诊断信息将显示在 YOURSITE.com/logs 中。请务必 :white_check_mark: YOURSITE.com/logs 底部的 warnings 框。

我们将向日志记录一条警告,其中包含 SSO 有效负载的完整转储:

1 Like