如何从 OKTA 获得用于 unit/integration 测试的 SAML 响应

How to get a SAML Response from OKTA for unit/integration testing

我正在从事他们通过 OKTA 的 SAML 进行身份验证的项目。我已经成功地将 SAMLResponse(通过 POST 方法)发送到网站的集成工作。

以真正的 TDD 方式,我开始编写一些单元测试。我的单元测试采用 SAMLResponse(Base64 编码)。然而,我所有的单元测试都有效,因为 SAMLResponse 只有几分钟的生命周期(过期),我的单元测试在几分钟后中断。

所以我需要定期登录 OKTA,然后使用 Chrome 开发工具捕获发送到我的开发站点的流量。然后,我将 SAMLResponse 复制并粘贴到我的单元测试中,然后返回通过单元测试。显然,这不是理想的情况。

所以我的问题是如何以自动方式(最好是在 C# 中)登录 Okta 以获得 SAMLResponse?我假设有一些 URL 我可以 POST 使用用户名和密码并取回 SAMLReponse。我所有的 Fiddler 尝试理解所需的通信都让我感到沮丧。我正在寻找您可能拥有的任何指导。提前致谢。

有多种方法可以进行您建议的那种测试,以下是立即想到的方法:

  1. 使用 RestSharp 或类似的库在 C# 中编写一个简单的 HTTP "User Agent"。
  2. 针对外部模拟 IdP 构建集成测试。
  3. 使用您自己的密钥修改 SAMLResponse 和 re-sign。

我在下面详细介绍了每种方法。

写一个简单的 HTTP "User Agent"

我建议这种方法,我建议采用以下两种方法之一:

  1. 编写一个通用的用户代理来检测并填写用户名和密码表单字段。我在 Python 中写了一个名为“saml-messenger" that takes this approach, the core code is in the file named messenger.py.
  2. 的工具
  3. 使用 Okta 的 API 获取会话令牌并使用它来获取 SAMLResponseokta-aws-cli-assume-role project takes this approach. The code for fetching the session token and exchanging that session token for a SAMLResponse 都可以在 src/main/java/com/okta/tools/awscli.java 文件中找到。

第一种方法更通用,应该适用于任何具有用户名和密码字段的 IdP。第二种方法可能是您正在寻找的方法,但特定于 Okta。

无论哪种情况,我都建议在 Okta 中创建一个特殊用户,该用户被锁定并仅用于测试。

针对模拟 IdP 构建集成测试

这种方法会让你设置一个模拟 IdP,它会在没有身份验证的情况下给你一个 SAMLResponse,我以前使用 saml-idp 项目来做到这一点。

这种方法的好处是,它应该需要更少的 C# 来编写,但代价是承担另一个依赖项。

修改 re-signing SAMLResponse

我真的只是为了完整起见才在此处包含此选项,并警告您不要尝试这种方法。了解 SAML 会让您直面疯狂和后悔。我不建议使用这种方法。

我想出了一个可行的解决方案并想与社区分享。 我不确定根据其他作者 (Joël Franusic) 的有用反馈回答我自己的问题的协议。如果我违反协议,请告诉我。

感谢 Joël Franusic 的指点。我实施了他的 1.2 解决方案(带有 Okta 客户端的用户代理)在他的参考资料和 Okta 网站上的其他一些文档之间,我最终能够拼凑出工作代码。

private static async Task<string> GetTestSamlResponse()
    {
        try
        {
            // settings specific to my Okta instance
            string username = "USERNAME GOES HERE";
            string password = "PASSWORD GOES HERE";
            var apiToken = "API TOKEN GOES HERE";

            // this is the unique domain issued to your account.  
            // If you setup a dev account you'll have a domain in the form https://dev-<AccountNumber>.oktapreview.com.
            // account number is a unique number issues by Okta when you sign up for the account
            var baseUrl = "YOUR BASE URL GOES HERE";

            // In Okta Admin UI, click "Applications" in main menu, choose your app, click "Sign On" tab.  Under Sign On Methods, then under SAML 2.0, click "View Setup Instructions"
            // Get the url called "Identity Provider Single Sign-On URL", paste it in th below line
            var ssoUrl = "YOUR SSO URL GOES HERE";

            // construct an Okta settings object
            var settings = new Okta.Core.OktaSettings
            {
                ApiToken = apiToken,
                BaseUri = new Uri(baseUrl)
            };

            // get session token from Okta
            var authClient = new Okta.Core.Clients.AuthClient(settings);
            var authResponse = authClient.Authenticate(username, password);
            var sessionToken = authResponse.SessionToken;

            // start session and get a cookie token
            var sessionsClient = new Okta.Core.Clients.SessionsClient(settings);
            var session = sessionsClient.CreateSession(sessionToken);
            var cookieToken = session.CookieToken;

            // using the cookie token, get the SAMLResponse from Okta via a HTTP GET.
            var httpClient = new System.Net.Http.HttpClient();

            // add User-Agent header, because apparently Okta is expecting this information.  
            // If you don't pass something, the Okta site will return a 500 - Internal Server error
            httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "UnitTest");

            // add the cookie token to the URL query string
            string url = string.Format("{0}?onetimetoken={1}", ssoUrl, cookieToken);

            // do the HTTP GET
            using (var response = await httpClient.GetAsync(url))
            {
                if (response.StatusCode == HttpStatusCode.OK)
                {
                    // read the HTML returned
                    string html = await response.Content.ReadAsStringAsync();

                    // parse the HTML to get the SAMLResponse (using HtmlAgilityPack from NuGet)
                    HtmlAgilityPack.HtmlDocument htmlDoc = new HtmlAgilityPack.HtmlDocument();
                    htmlDoc.LoadHtml(html);
                    // from the input field called SAMLResponse, get the "value" attribute
                    string samlResponse = htmlDoc.DocumentNode.SelectSingleNode("//input[@name='SAMLResponse']").Attributes["value"].Value;
                    return samlResponse;
                }
                else
                    throw new Exception(string.Format("Error getting SAML Response {0}", response.StatusCode));
            }
        }
        catch (Exception ex)
        {
            throw;
        }
    }