在 Delphi 中通过 Entra ID、Okta 和 AD FS 实现 SAML 单点登录

· 组件
在 Delphi 中通过 Entra ID、Okta 和 AD FS 实现 SAML 单点登录

迟早会有一个大客户提出这个问题:我们的员工能否用公司账户登录你们的应用?他们指的不是又一个用户名和密码,而是他们已经用于其他一切事务的 Microsoft Entra ID、Okta 或 AD FS 登录方式,配合他们自己的密码策略、自己的双因素认证,以及一个在有人离职当天就能关闭账户的统一入口。

客户身份团队期望的答案是 SAML 2.0。在新登录组件概览一文中,SAML 只占了一段篇幅。这篇文章讲的是完整流程:TsgcSAMLServiceProvider 做了什么、登录页面和 Assertion Consumer Service 的代码、如何在常见身份提供商中注册你的应用程序,以及如何在今天就无需任何账户完成全部测试。

SAML 登录的工作原理

共有三方参与。你的应用程序是服务提供商(SP)。客户的目录是身份提供商(IdP)。浏览器在两者之间传递消息,因此你的服务器和 IdP 永远不会直接通信。

  1. 用户打开你的登录 URL。你的应用程序构建一个 AuthnRequest,并将浏览器重定向到 IdP。
  2. IdP 按照公司设定的任何密码、MFA 或条件访问规则完成用户登录。
  3. IdP 返回一个已签名的 SAMLResponse,浏览器将其 POST 到你的 Assertion Consumer Service(ACS)URL。
  4. 你的应用程序验证该响应,并为其中指明的用户创建自己的会话。

第四步正是 SAML 实现容易出错的地方,而这正是该组件为你完成的部分。

服务提供商,逐步讲解

TsgcSAMLServiceProvider 不是一个 HTTP 服务器。它负责构建和检查 SAML 消息,你需要从你的应用程序已有的服务器的请求处理程序中调用它,例如 TsgcWebSocketHTTPServerTsgcHTTPServer

在启动时一次性完成 EntityIDAssertionConsumerServiceURLLoadIdPMetadata 的设置后,登录页面和 ACS 可以放在一个请求处理程序中:

uses
  sgcAuth_SAML_SP;

procedure TMyApp.OnCommandGet(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
  vRelayState, vRequestID: string;
  oResult: TsgcSAMLResult;
begin
  if ARequestInfo.Document = '/saml/login' then
  begin
    // 1. send the browser to the identity provider
    vRelayState := NewRelayState;
    AResponseInfo.Redirect(FSAML.GetAuthnRequestRedirectURL(vRelayState,
      vRequestID));
    // 2. keep the request id, the response must answer it
    AddPendingRequest(vRelayState, vRequestID);
  end
  else if (ARequestInfo.Document = '/saml/acs') and
    SameText(ARequestInfo.Command, 'POST') then
  begin
    // 3. the browser posts SAMLResponse and RelayState back
    vRelayState := ARequestInfo.Params.Values['RelayState'];
    vRequestID := TakePendingRequest(vRelayState);
    oResult := TsgcSAMLResult.Create;
    try
      if FSAML.ProcessResponse(ARequestInfo.Params.Values['SAMLResponse'],
        vRelayState, vRequestID, oResult) then
      begin
        // 4. signed in: create your own session for this user
        CreateUserSession(AResponseInfo, oResult.NameID, oResult.SessionIndex);
        AResponseInfo.Redirect('/');
      end
      else
        AResponseInfo.ResponseNo := 403; // log oResult.ErrorMessage
    finally
      oResult.Free;
    end;
  end;
end;

NewRelayStateAddPendingRequestTakePendingRequestCreateUserSession 代表你自己的代码:一个 GUID、一个以 RelayState 为键、每个请求 id 只发放一次的线程安全列表,以及你应用程序的会话 cookie。属性以 Name=Value 的行到达,因此 oResult.Attributes.Values['email'] 可以按名称读取其中一个。Entra ID 使用诸如 http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress 这样的 claim URI 来命名它们。

RelayState 不受 IdP 签名的保护。应将其用作查找你自己待处理请求的键,而绝不能用作未经检查就重定向的 URL。

在身份提供商中注册你的应用程序

GetMetadata 返回服务提供商的元数据:你的 entity ID 以及带有 HTTP-POST 绑定的 ACS URL。将其发布在诸如 /saml/metadata 这样的 URL 上,或保存为文件,然后提供给 IdP。每个身份提供商都会要求相同的两个值,即 SP 的 entity ID 和 ACS URL,因此以下说明主要是关于每个控制台将它们保存在何处。在所有情况下,都应保持断言加密处于关闭状态。

无论是哪个 IdP,最后一步都是一样的:将其元数据传给 LoadIdPMetadata。当文档描述了多个实体时,第二个参数用于选择你的那个。

ProcessResponse 检查了什么

SAML 响应是一份已签名的 XML 文档,而大多数广为人知的 SAML 漏洞,都是想办法让服务提供商读取到与实际签名内容不同的东西。只有当以下每一项检查都通过时,响应才会被接受:

解析器还会拒绝 DOCTYPE 声明,因此不存在外部实体,并且会限制文档的大小和嵌套深度。颁发者(issuer)必须是你所配置的那个 IdP。第一项失败的检查会停止验证,其原因会记录在 ErrorMessage 中:记录下来,并向用户显示一个简单的“登录失败”页面。

无需账户即可试用

你不需要 Entra ID 租户就能看到 SAML 的运行效果。Mock SAML 是 mocksaml.com 上提供的一个免费测试身份提供商。它接受任意服务提供商,并从 AuthnRequest 中获取受众和 ACS URL,因此无需任何注册。

演示程序 Demos\26.Authentication\03.SAML_ServiceProvider 是一个基于 TsgcWebSocketHTTPServer 的完整服务提供商,在 http://localhost:8090 上提供 /login/acs/metadata 端点:

  1. 编译该演示程序,并将 libcrypto-3.dll 和 libssl-3.dll 保留在可执行文件旁边。它们位于演示程序文件夹中,OpenSSL 用它们验证 RSA 签名。
  2. 点击 Load IdP metadata。默认来源是 mocksaml.com 的元数据 URL。
  3. 点击 Start,然后点击 Open Browser,并按照登录链接操作。
  4. 在 mocksaml.com 上,输入 example.com 域下的任意用户名和任意密码。
  5. 浏览器返回到 ACS,页面会显示 NameID、SessionIndex 以及 id、email、firstName 和 lastName 等属性。

确认可以正常工作后,打开 http://localhost:8090/metadata,将其注册到你真实的 IdP,在演示程序中加载 IdP 元数据,然后再次登录。对于 AD FS,请先在启用 SSL 的情况下运行该演示程序,因为 AD FS 只接受 https。

当前的限制

文档

获取方式

TsgcSAMLServiceProvider 包含在 sgcWebSockets 的 Enterprise 和 All-Access 版本中,支持 Delphi 和 C++ Builder,同一个组件也是 sgcWebSockets .NET 的一部分。如果你只需要身份验证功能,sgcAuth 套件连同其他登录组件一起提供了它。所在单元为 sgcAuth_SAML_SP,在你将该组件放到窗体上之前,现有应用程序不会有任何变化。

延伸阅读

观看视频

eSeGeCe 频道上有一段简短视频,标题为“SAML single sign-on in Delphi with Entra ID, Okta and AD FS”。视频展示了 IDE 中的代码,以及使用该演示程序针对 mocksaml.com 进行的实时登录。

有疑问、反馈,或需要帮助接入你的身份提供商?联系我们。你会收到编写这些代码的人给出的回复。