我们如何添加Single Sign-On SAML支持并在生产中对其进行调试
团队可能会使用数十种服务和应用程序来完成任务。 每个服务都要求他们注册并记住复杂的密码,而最终每个服务的内部所有者必须手动处理登录凭据和帐户设置。
这就是SAML的用武之地,旨在帮助公司和服务拥有更好的身份验证流程,而不必手动注册每个人。 以下文章将介绍如何在公司中实施SAML单一登录,并从中受益。
让我们潜入。
什么是SAML,它有什么用?
SAML,S ecurity 甲 ssertion 中号 arkup 大号 anguage,是企业和服务提供商之间交换认证和授权数据的开放标准的数据格式。 这是一个安全协议,类似于OpenId,OAuth(我们也在此处写过),Kerberos和其他协议。
SAML地址的主要用例之一是跨服务的单点登录(SSO),以通过其他服务提供简单的登录体验。 例如,使用Google帐户注册Stackoverflow的经验。 通过应用SSO,公司可以使用该协议进行访问控制。
这种方法为用户和公司都提供了巨大的价值。 用户不必处理和记住各种应用程序的密码,而仍然可以为他们提供所需的信息和服务。 公司可以有一个过程来轻松识别内部用户,从而为他们提供所需的数据。 同样,如果员工离开公司,则更容易撤销一个主帐户,而不必为该用户有权访问的每种服务进行遍历和搜索多个帐户。
实施SAML
SAML用于在身份验证过程中定义2个参与方:
- 服务提供商–用户要登录的服务
- 身份提供者–想要访问服务的用户
这两个提供程序通过将XML元数据文件从一个传递到另一个来建立信任。 这是一次性配置步骤。 之后,当用户尝试访问该服务时,SAMLRequest和SAMLResponse是在两个提供程序之间发送并进行实际身份验证的XML字符串。 这是一个一次性的过程,它使用户可以为所需的应用程序执行SSO。
不是每个人都适合SAM(L)E
这个过程听起来很简单,但并非总是如此。 尽管大多数公司使用可以轻松测试的单个身份提供商(IdP),但服务提供商可能需要支持各种不同的供应商。
这是我们在为正在构建的工具实施SAML支持时在OverOps遇到的问题之一。 我们拥有不同的客户,从拥有5名员工的小型初创公司到拥有数千名用户的企业公司。 尽管我们支持使用密码登录以及使用Google或Github的OAuth登录,但我们希望拥有现有SAML解决方案的公司能够无缝登录到我们的服务。 因此,我们需要支持多个身份提供者,例如:Okta,PingOne,SSOCircle,OneLogin等。
这就是为什么我们决定使用应支持所有功能的opensaml项目的原因。 SAML是一个包含许多详细信息和可能的供应商的协议,每个公司都可以通过自己的怪癖和细微差别对它进行不同的配置-我们希望支持所有这些。
生产测试
当我们首次实现新的SAML功能时,我们在Beta中推出了该功能,并尝试对尽可能多的供应商进行测试。 我们知道在此过程中可能会遇到一些差异,并希望了解哪些问题可能会出现。
对于大多数公司而言,此过程可以完美地进行,但是我们确实遇到了一些问题。 一些公司的字符串使用了不同的编码,其他公司没有将标准属性(名字,姓氏等)传递给标准属性名称,而一些公司具有自定义实现。
由于我们在自己的生产环境中使用了OverOps ,因此我们可以轻松地看到SAMLResponse对于某些客户来说是失败的,以及导致他们失败的原因。 因此,在开发环境中重现错误非常简单,因此我们使用给定的输入创建了单元测试。
这就是说OverOps提供一种主动方法的意思。 我们不仅知道我们的一些客户在使用新功能时遇到问题,而且在他们甚至联系我们以报告错误首先发生之前就已经看到了错误的根本原因。
借助OverOps ,我们能够过滤和监视趋势以得出新的见解。 例如,我们能够看到与此登录失败相关的异常的趋势,并确保在部署修补程序后该异常不会再次出现。
在OverOps中实施SAML
首先,我们使用Dead Simple SAML 2.0 Client作为参考,因为我们要支持的第一家供应商是Okta。 此仓库的作者Martin Laporte实施了一种特殊的处理方式,并且简单地使用了有效的SAML。 很快,我们创建了一个定制版本以适应我们自己的需求。
此外,由于SAML解决方案通常不需要支持多种类型的供应商,因此通常将IdP元数据放入XML文件中并仅进行加载。 但是,由于在我们的案例中,我们希望支持为每个公司在线配置新的IdP元数据的选项,并为合适的公司加载合适的元数据,因此我们将其存储在内部数据库中,公司名称是关键,而元数据是价值。
因此,我们的代码大致如下所示:
String companyMetadata = DB.getCollection(“SAMLMetadata”).getByKey(companyName);
然后,我们可以使用StringReader将读取器传递给SamlClient.fromMetadata:
SamlClient samlClient = SamlClient.fromMetadata("", assertionConsumer, new StringReader(idpMetadata));
最后,我们使用前面讨论过的编码SAMLResponse调用encodeAndValidateSamlResponse:
samlClient.decodeAndValidateSamlResponse(encodedSAMLResponse)
encodeAndValidateSamlResponse的作用是运行此代码,该代码基本上检查响应是否符合SAMLResponse的标准格式,并对其执行base64解码,解析和验证步骤。
由于可以从具有不同设置的不同供应商处收到响应,因此每个步骤都可能失败。 这就是为什么我们将它包装在带有多个catch子句及其适当异常的try子句中的原因:
public SamlResponse decodeAndValidateSamlResponse(String encodedResponse) throws SamlException
{
String decodedResponse;
try
{
decodedResponse = new String(Base64.decode(encodedResponse), "UTF-8");
}
catch (UnsupportedEncodingException ex)
{
throw new SamlException("Cannot decode base64 encoded response", ex);
}
logger.trace("Validating SAML response: " + decodedResponse);
Response response;
try
{
DOMParser parser = new DOMParser();
parser.parse(new InputSource(new StringReader(decodedResponse)));
response =
(Response)Configuration.getUnmarshallerFactory()
.getUnmarshaller(parser.getDocument().getDocumentElement())
.unmarshall(parser.getDocument().getDocumentElement());
}
catch (IOException ex)
{
throw new SamlException("Cannot decode xml encoded response", ex);
}
catch (SAXException ex)
{
throw new SamlException("Cannot decode xml encoded response", ex);
}
catch (UnmarshallingException ex)
{
throw new SamlException("Cannot decode xml encoded response", ex);
}
validateResponse(response);
validateAssertion(response);
validateSignature(response);
Assertion assertion = response.getAssertions().get(0);
return new SamlResponse(assertion);
}
并非一切都完美
这些集成包括很多技术细节,记录每个会话会给您的日志增加大量噪音。 在某些情况下,您甚至都不知道应该记录什么,因为这些错误是完全意外的,只能在生产中出现。 这就是为什么OverOps对于及时解决我们遇到的错误至关重要。 要查看其工作原理, 请查看此实时演示 。
同样,尽管我们的案例专门针对SAML,但它代表了公司在开发具有许多不同活动部件的大型系统时所面临的挑战。 作为一家公司,我们希望支持尽可能多的工具,插件,集成和其他功能,尽管客户总是对的,但他们的输入可能是错误的。 或至少是意外的。
最后的想法
实施SAML只是公司想要,需要或应该与之融合的要素生态系统不断发展的一小步。 我们可以花时间尝试预测不同的边缘情况,但这是不切实际的,而且在大多数情况下–您无法预见未来。 这就是为什么您需要工具来帮助您应对产品的增长,在发生错误时检测和识别根本原因(在客户抱怨之前)的原因。