IdentityServer SSO - 受信任的应用程序

IdentityServer SSO - Trusted application

我需要对来自我的应用程序(使用 ASPNET 会话状态的身份提供者)的用户进行 SSO(单点登录),并将他们重定向到我的另一个应用程序(服务提供者),该应用程序配置为使用隐式流身份服务器4。我需要在不要求用户重新登录且不提供用户密码的情况下实现这一点。

我最初的想法是,我可以使用身份提供者的客户端密码将用户重定向到 IdentityServer4 身份验证端点,并将访问令牌作为查询参数,然后使用自定义验证器或扩展授权来发出一个与服务提供商应用程序一起使用的身份令牌,无需同时提供用户密码。

我已经设法向身份提供者颁发访问令牌,然后将用户重定向到 IdentityServer4,但事实证明颁发身份令牌对我来说很困难。我已经倾注了样本和文档,至少可以说我很困惑。

我正在寻找针对这种情况的适当方法的方向,也许是 C# 中的综合示例。我开始明白我可以使用混合流程来颁发访问令牌和身份令牌。我认为我最大的困难是如何重定向用户并基于访问令牌向用户颁发身份令牌(如果这甚至是一种可接受的方法)。

简单地说:我想基于对身份提供者的信任(通过客户端密码?)将用户从应用程序 A 重定向到 IdentityServer4 再到应用程序 B。

注意:我知道这可以被认为是一个基于意见的问题,但根据我的研究,我相信只有一个最佳实践,这就是我所要求的。

我想我可以先处理应用程序 A 的身份验证,然后再转到下一个应用程序...

应用程序 A --> IdentityServer --> 应用程序 A --> 应用程序 B。

您可以在 returnUrl 中包含一些自定义参数,应用程序 A 可以从 IdentityServer 读取 return,这将触发重定向到应用程序 B。

我设法通过以下流程让它工作:

  1. 在应用程序 A(身份提供者)中授权用户
  2. 通过令牌端点和共享密钥从 Identity Server 4 获取访问令牌。
  3. 添加访问令牌作为查询字符串参数,因为 header 不会在重定向时保留。
  4. 将用户重定向到接受用户名等识别信息的帐户控制器方法。此方法受自定义中间件 class 保护,该中间件检查访问令牌参数的查询字符串。如果令牌存在,则将其添加到身份验证header;这授权用户点击此控制器方法。
  5. 控制器方法随后将用户登录并将他们重定向到 /connect/authorize/login 端点。
  6. 最后,登录端点设置 cookie 并将用户重定向到应用程序 B(服务提供商),其 URL 是通过 redirect_uri 查询参数指定的。

共享密钥配置:

向客户端添加适当的授权类型、机密和新范围名称。新范围将有助于调试日志中的访问令牌问题(尤其是当您有多个应用程序访问您的 ID4 服务器时)。还要确保将服务提供商的 URL 添加到客户端 RedirectUris,否则您将收到 "invalid redirect" 错误。

            AllowedGrantTypes = new List<string> { GrantType.Implicit, GrantType.ClientCredentials },
            ClientSecrets = new List<Secret> {
                new Secret(_clientSecrets.ExternalIdpSecret.Sha256(), clientID)
            },
            AllowedScopes = new List<string>
            {
                "newScopeName"
            },
            RedirectUris = new List<string>
            {
                $"http://localhost:<portnumber>"
            }

接下来,添加您的自定义中间件。

public class QueryStringOAuthBearerMiddleware
{
    private readonly RequestDelegate next;

    public QueryStringOAuthBearerMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        this.BeginInvoke(context);
        await this.next.Invoke(context);
        this.EndInvoke(context);
    }

    private void BeginInvoke(HttpContext context)
    {
        if (context.Request.Query.ContainsKey("accesstokenparametername"))
        {
            var accessToken = context.Request.Query.First(p => p.Key == "accesstokenparametername");

            if (!string.IsNullOrEmpty(accessToken.Value))
            {
                context.Request.Headers.Add("Authorization", "Bearer " + accessToken.Value);
            }
        }
    }

    private void EndInvoke(HttpContext context)
    {
    }
}

并将中间件添加到您的配置中。

        app.UseMiddleware<QueryStringOAuthBearerMiddleware>();

创建您的登录方法。

    [HttpGet]
    [Authorize]
    public async Task<IActionResult> Login2(string userName, string returnURL)
    {
        await _httpContextWrapper.SignInAsync(userName);

        return Redirect(returnURL);
    }

客户端应用程序 (IDP) 的配置:

您的客户端代码应如下所示:

var disco = await DiscoveryClient.GetAsync("http://localhost:<portnumber>");
var tokenClient = new TokenClient(disco.TokenEndpoint, "clientIdentifier", "IUsedAGuidHere");
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("newScopeName");

var redirectURL = string.Format("http://localhost:2228/account/Login2?userName=<UserIDValue>&returnURL={1}&accesstokenparametername={0}",
            tokenResponse.AccessToken,
            Server.UrlEncode(
                string.Format("/connect/authorize/login?client_id={3}&redirect_uri={2}&response_type=id_token%20token&scope=<ImplicitFlowScopes>&state={0}&nonce={1}",
                CryptoRandom.CreateUniqueId(),
                CryptoRandom.CreateUniqueId(),
                Server.UrlEncode("http://localhost:<PortNumber>"),
                "ClientIdentifier")));

Response.Redirect(redirectURL, false);

注意:请理解您将无法使用此代码 AS-IS 并使其正常工作。我对其进行了大量修改以保护我的资源安全。