DiscourseConnect 是 Discourse 的一项核心功能,允许您配置“单点登录 (SSO)”,将 Discourse 的所有用户注册和登录完全外包给另一个站点。此功能提供给我们的标准、商业和企业托管客户。
(2021 年 2 月)“Discourse SSO”现已更名为“DiscourseConnect”。如果您运行的是旧版本的 Discourse,则以下设置将被命名为
sso_...
而不是discourse_connect_...
问题所在
许多希望与 Discourse 站点集成的站点都希望将所有用户注册保留在一个单独的站点中。在这种设置中,所有登录操作都应外包给该不同的站点。
如果我希望将 SSO 与现有身份验证结合使用,该怎么办?
DiscourseConnect 的目的是取代 Discourse 身份验证,如果您想添加新的提供程序,请参阅现有的插件,例如:Discourse VK 身份验证 (vkontakte)
启用 DiscourseConnect
要启用 DiscourseConnect,您需要填写 3 个设置:
enable_discourse_connect
:必须启用,全局开关
discourse_connect_url
:用户尝试登录时将被发送到的站外 URL
discourse_connect_secret
:用于对 SSO 有效负载进行哈希处理的秘密字符串。确保有效负载的真实性。
一旦 enable_discourse_connect
设置为 true:
- 单击登录或头像,将您重定向到
/session/sso
,然后将用户重定向到带有签名有效负载的discourse_connect_url
。 - 不允许用户“更改密码”。该字段已从用户配置文件中删除。
- 用户将无法再使用 Discourse 身份验证(用户名/密码、谷歌等)
如果您不小心选中了它怎么办?
请参阅:使用只读模式或无效的 SSO 配置将自己锁定后,以管理员身份重新登录
在您的站点上实现 DiscourseConnect
Discourse 使用电子邮件将外部用户映射到 Discourse 用户,并假定外部电子邮件是安全的。如果您在将电子邮件地址发送到 DISCOURSE 之前未对其进行验证,您的站点将非常容易受到攻击!
或者,如果您坚持发送未经验证的电子邮件,请务必设置 require_activation=true
,这将强制 Discourse 验证所有电子邮件。我们仍然强烈建议您不要这样做,因此如果您继续启用该设置,您将承担巨大的风险。
Discourse 会将客户端重定向到带有签名有效负载的 discourse_connect_url
:(假设 discourse_connect_url
是 https://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 编码的。
被调用的端点必须
- 验证签名:确保
PAYLOAD
的 HMAC-SHA256(使用discourse_connect_secret
作为密钥)等于sig
(sig
将进行十六进制编码)。 - 执行它必须执行的任何身份验证
- 创建一个至少包含 nonce、email 和 external_id 的新的 URL 编码有效负载。您还可以提供一些附加数据,以下是 Discourse 可以理解的所有密钥的列表:
- nonce 应从输入有效负载中复制
- email 必须是经过验证的电子邮件地址。如果电子邮件地址未经验证,请将 require_activation 设置为“true”。
- external_id 是用户唯一的任何字符串,即使他们的电子邮件、姓名等发生更改,该字符串也不会更改。建议的值是您数据库的“id”行号。
- 如果用户是新用户或设置了
SiteSetting.auth_overrides_username
,username 将成为 Discourse 上的用户名。 - 如果用户是新用户或设置了
SiteSetting.auth_overrides_name
,name 将成为 Discourse 上的全名。 - 如果用户是新用户或设置了
SiteSetting.discourse_connect_overrides_avatar
,avatar_url 将被下载并设置为用户的头像。 - avatar_force_update 是一个布尔字段。如果设置为 true,它将强制 Discourse 更新用户的头像,无论
avatar_url
是否已更改。 - 如果用户是新用户、他们的个人简介为空或设置了
SiteSetting.discourse_connect_overrides_bio
,bio 将成为用户个人简介的内容。 - 其他布尔字段(“true”或“false”)包括:admin、moderator、suppress_welcome_message
- Base64 编码有效负载
- 使用
discourse_connect_secret
作为密钥,使用 Base64 编码的有效负载作为文本,计算有效负载的 HMAC-SHA256 哈希 - 使用
sso
和sig
查询参数重定向回return_sso_url
(http://discourse_site/session/sso_login?sso=payload&sig=sig
)
Discourse 将验证 nonce 是否有效,如果有效,它将立即过期,使其无法再次使用。然后,它将尝试:
- 通过在
SingleSignOnRecord
模型中查找已关联的 external_id 来登录用户 - 使用提供的电子邮件登录用户(更新 external_id)(除非 require_activation = true)
- 为用户提供(电子邮件、用户名、姓名)更新 external_id 创建一个新帐户
安全问题
nonce(一次性令牌)将在 10 分钟后自动过期。这意味着一旦用户被重定向到您的站点,他们有 10 分钟的时间登录/创建新帐户。
该协议可以安全地防止重放攻击,因为 nonce 只能使用一次。nonce 与当前浏览器会话绑定,以防止 CSRF 攻击。
指定组成员资格
如果指定了 discourse connect 覆盖组 选项,Discourse 将考虑在 groups
中传递的以逗号分隔的组列表。
除了 groups
之外,您还可以使用 add_groups
和 remove_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
在另一端
- 使用 HMAC-SHA256 验证 有效负载,如果 sig 不匹配,则进程中止。
- 通过反转上述步骤提取 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_1
的UserCustomField
的值。 - 您可以传递
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
中。请务必
YOURSITE.com/logs
底部的 warnings
框。
我们将向日志记录一条警告,其中包含 SSO 有效负载的完整转储:
- 每次启动 DiscourseConnect 进程时,我们都会向日志记录一条警告,其中包含 DiscourseConnect 有效负载的完整转储
- 每次用户未能完成 DiscourseConnect(由于 nonce 过期或 ip 阻止)