【问题标题】:how to implement OpenID Connect from a private provider in the c# asp.net如何在 c# asp.net 中从私有提供程序实现 OpenID Connect
【发布时间】:2020-12-16 19:27:13
【问题描述】:

我有一个 ASP.NET MVC 应用程序,需要集成来自 Private OpenID Connect (OIDC) Provider 的 OpenID Connect 身份验证,流程包含以下步骤:

  1. 用户点击登录

  2. 它将使用以下HTTP GET 请求将用户重定向到私有 OIDC 站点进行身份验证:

  3. 在私有 OIDC 站点成功登录后,它将重定向回我的站点并获取带有 code 结果的 uri,如下所示:

  4. 然后我需要使用上面的code 并调用HTTP POST 到私有ODIC 令牌端点以获取该用户的访问令牌。

那么,我的questions #1 是:如何在c# asp.net app 中实现这个?

另外,我在 Postman “Get New Access Token”中试过这个,我得到了令牌。

如你所见,在我提供所有参数并单击请求令牌后,它会弹出登录 winnow, 登录成功后显示token

我的questions #2 是:类似于问题#1,有没有在c# asp.net 应用程序中实现这个? 就像在 asp.net mvc 应用程序中一样,在第一张图片中添加一个带有 url 的链接按钮,当用户单击时,它将使用 code 将其重定向回 myapp,然后使用此代码进行 HTTP POST调用 stpe3

【问题讨论】:

标签: c# asp.net oauth-2.0 postman openid-connect


【解决方案1】:

设置 identifyServer4:IdentityServer4 是一个用于 ASP.NET 的 OpenID Connect 和 OAuth 2.0 框架

您可以在此处找到有关如何使用 identifyServer4 的文档: https://identityserver4.readthedocs.io/en/latest/ https://identityserver4.readthedocs.io/en/latest/quickstarts/3_aspnetcore_and_apis.html

IdentityServer4 提供的一些功能是:

身份验证即服务

所有应用程序(网络、本机、移动、服务)的集中登录逻辑和工作流程。 IdentityServer 是经过官方认证的 OpenID Connect 实现。

单点登录/注销

在多种应用程序类型上单点登录(和注销)。

API 访问控制 为各种类型的客户端的 API 颁发访问令牌,例如服务器到服务器、Web 应用程序、SPA 和本机/移动应用程序。

联合网关

支持 Azure Active Directory、Google、Facebook 等外部身份提供程序。这使您的应用程序免受如何连接到这些外部提供程序的详细信息。

专注于定制

最重要的部分 - IdentityServer 的许多方面都可以定制以满足您的需求。由于 IdentityServer 是一个框架,而不是盒装产品或 SaaS,因此您可以编写代码以使系统适应您的场景。

成熟的开源

IdentityServer 使用宽松的 Apache 2 许可证,允许在其上构建商业产品。它也是提供治理和法律支持的 .NET 基金会的一部分。 免费和商业支持

【讨论】:

    【解决方案2】:

    您可以找到open source example of this on GitHub。其许可证非常宽松,并且有据可查。我已经在各种研讨会和培训中使用过它,因此大多数错误都已解决。我建议你深入研究一下。不过为了完整起见,我将在这里描述一般过程,并以此作为解释的基础。

    任何实现 OpenID Connect 代码流的 Web 应用程序都包括两个部分:

    1. 流程的开始和
    2. 回调的处理

    执行这两件事的应用程序称为“客户端”或“依赖方”。此客户端使用 OpenID Connect 协议与之通信的东西称为 OpenID Connect Provider (OP),通常也称为 Identity Provider (IdP)。

    客户端实现的第一部分将显示一个包含按钮的视图。此按钮将是典型的“登录”或“登录”按钮。请注意,这是可选的,如果应用程序检测到用户没有会话,它可能会立即将用户重定向到 OP。但是,鉴于您上面的问题,您的情况并非如此,客户端将首先呈现一个显示此类按钮的视图。视图可能如下所示:

    <div>
        @if(Session.Count == 0) {
            <p>
                This is a demo application to demonstrate the use for OAuth2 
                and OpenID Connect. 
            </p>
    
            <p>
                Pressing Sign In will redirect you to @ViewData["server_name"] 
                and authorize the application to access your profile info. The 
                data will only be used to demonstrate the possibilities of the 
                OpenID Connect protocol and will not be stored. Be sure to 
                revoke access when you are satisfied.
            </p>
            <div>
                <a href="/login">Sign In</a>
            </div>
        } else {
          // ...
        }
    </div>
    
    

    此视图将由一个非常基本的控制器呈现,该控制器连接在Global.asax.cs 中建立的路由配置中。单击登录按钮时,OpenID Connect 部件将启动。处理此请求的控制器将简单地重定向到 OP 的授权端点。在最基本的情况下,这可能看起来像这样:

    public class LoginController : Controller
    {
        private static string start_oauth_endpoint = Helpers.Client.Instance.GetAuthnReqUrl();
    
        public ActionResult Index()
        {
            return Redirect(start_oauth_endpoint);
        }
    }
    

    有趣的部分是如何获得授权端点。这可以是硬编码的,在Web.config 中定义,或者从 OP 的元数据中获取。在我上面引用的示例中,它在应用启动时获取 OP 的元数据。这是在位于 Web 应用程序的 App_Start 目录中的 AppConfig 中完成的。这将使用/.well-known/openid-configuration 向颁发者ID(位于Web.config)执行HTTP GET 请求。在应用启动时获取这些元数据而不是将其全部放入配置中的原因是为了减少 OP 和客户端的耦合。

    上面截图中执行的重定向将有一些重要的查询字符串参数。其中一些将在设计时已知,并且将被硬编码。其他将在Web.config 中配置。有些将在运行时动态计算。下面列出了这些:

    client_id
    此 MVC Web 应用的客户端 ID。
    response_type
    OP 应该使用的响应类型。在您的情况下,这将始终是 code
    scope
    客户端请求的访问范围。这将至少包括openid
    redirect_uri
    在对客户端进行身份验证和授权后,OP 应将用户发送到的重定向 URI。

    也可以发送其他请求参数。为了帮助您确定要发送哪些内容以及它们对流程的影响,请查看oauth.tools。这就像“OAuth 和 OpenID Connect 的邮递员”。这是梦幻般的;你会喜欢的。在那里,您可以使用各种参数形成各种 OAuth 和 OpenID Connect 流。

    一旦重定向到 OP,用户将进行身份验证。用户可能还必须同意客户端访问其受保护的资源。无论如何,OP 将在此之后将用户重定向到回调。这是实现的第二部分。

    在这里,我们将有一个CallbackController(或类似的东西)。它看起来像这样(最简单的形式):

    public class CallbackController : Controller
    {
        public ActionResult Index()
        {
            try
            {
                string responseString = Helpers.Client.Instance
                    .GetToken(Request.QueryString["code"]);
    
                SaveDataToSession(responseString);
            }
            catch (Exception e)
            {
                Session["error"] = e.Message;
            }
    
            return Redirect("/");
        }
    }
    

    这个 sn-p 的重要部分是它从查询字符串中获取code,并向 OP 的令牌端点(也通过解析 OP 的元数据定位)发出 HTTP POST 请求。如果成功,它会将响应保存在会话中以供以后使用。 GetToken 方法看起来像这样:

    public String GetToken(String code)
    {
        var values = new Dictionary<string, string>
        {
            { "grant_type", "authorization_code" },
            { "client_id", client_id},
            { "client_secret", client_secret },
            { "code" , code },
            { "redirect_uri", redirect_uri}
        };
    
    
        HttpClient tokenClient = new HttpClient();
        var content = new FormUrlEncodedContent(values);
        var response = tokenClient.PostAsync(token_endpoint, content).Result;
    
        if (response.IsSuccessStatusCode)
        {
            var responseContent = response.Content;
    
            return responseContent.ReadAsStringAsync().Result;
        }
    
        throw new OAuthClientException("Token request failed with status code: " + response.StatusCode);
    }
    

    这会将代码发送到 OP 并获得一个访问令牌、ID 令牌,也许还有一个刷新令牌作为交换。这段代码的重要部分是:

    • 内容是格式 URL 编码的不是 JSON。这是一个常见的错误。
    • 再次包含之前发送的 same 重定向 URI。这是为了匹配 OP 的两个请求。
    • grant_Type总是authorization_code
    • 客户端以某种方式进行身份验证。在这种情况下,通过在请求中包含与先前发送的相同的 client_id 以及 client_secret 表单元素中的秘密。
    • 使用的 HTTP 方法(如上所述)是 POST,而不是 GET。这也是一个常见的错误。

    在上面的示例中,我重定向回默认值HomeController。现在,执行 if 语句的 else 条件。在这里,它可以找到令牌:

    <div>
        @if(Session.Count == 0) {
            // ...
        } else {
            @if(Session["id_token"] != null) {
                <div>
                    ID Token:<br>
                    <pre>@Session["id_token"]</pre>
                </div>
            }
    
            @if(Session["access_token"] != null) {            
                <div>
                    Access Token:<br>            
                    <pre>@Session["access_token"]</pre>                
                </div>
            }
    
            @if(Session["refresh_token"] != null) {
                <div>
                    Refresh Token:<br>                
                    <pre>@Session["refresh_token"]</pre>
                </div>
            }
        }
    </div>
    

    这个例子比这个更详细,但它希望能给你一个想法。通读一遍,查看自述文件,并愉快地了解有关 OpenID Connect 的更多信息!

    【讨论】:

      【解决方案3】:

      您需要在不同的地方添加一些配置。我会尽量展示你需要的所有拼图。
      在我的示例中,我将使用面向 OIDC 的 IdentityServer4 的公共演示版本,以便您可以与工作版本进行比较。

      API
      在任何控制器(或方法)中,添加 [Authorize] 属性,因此这将需要有效的身份验证。
      如果您想更具体地说明用户可以执行的操作,您还可以添加策略。比如:

      [Authorize(Policy = "Read")]
      [ApiController]
      [Route("[controller]")]
      public class HelloWorldsController : ControllerBase
      {
          [HttpGet]
          public string Get()
          {
              return "Hello, World!";
          }
      }
      

      Startup.csConfigureServices 方法中,您需要添加类似的配置,如下所示:

      services
          .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
          .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
          {
              options.Authority = "https://demo.identityserver.io";
              options.Audience = "api";
          });
      
          // In case you want to work with policies
          services.AddAuthorization(options =>
          {
              options.AddPolicy("Read", policy => policy.RequireClaim("scope", "api"));
          });
      

      以上配置要编译,需要添加NuGet包Microsoft.AspNetCore.Authentication.JwtBearer

      Startup.csConfigure 方法中,您需要在app.UseAuthorization(); 之前添加app.UseAuthentication();

      MVC
      在任何控制器(或方法)中,添加[Authorize] 属性。每当您的 MVC 应用程序的用户点击具有此属性的方法时,将自动触发登录过程。
      为了演示这一点,我将把这个属性添加到一个方法中:

      [Authorize]
      public async Task<IActionResult> Privacy()
      {
          var httpClient = _httpClientFactory.CreateClient("ApiClient");
          var apiResult = await httpClient.SendAsync(
              new HttpRequestMessage(HttpMethod.Get, "/helloworlds"),
              HttpCompletionOption.ResponseHeadersRead);
          if (apiResult.IsSuccessStatusCode)
          {
              var content = await apiResult.Content.ReadAsStringAsync();
              ViewData.Add("apiResult", content); // Just to demonstrate
          }
      
          return View();
      }
      

      Startup.csConfigureServices 方法中,您需要添加类似的配置,如下所示:

      services.AddHttpContextAccessor();
      
      services.AddTransient<BearerTokenHandler>();
      
      services
          .AddHttpClient("ApiClient", client =>
          {
              client.BaseAddress = new Uri("https://localhost:5001");
          })
          .AddHttpMessageHandler<BearerTokenHandler>();
      
      services.AddHttpClient("IDPClient", client =>
      {
          client.BaseAddress = new Uri("https://demo.identityserver.io");
      });
      
      services
          .AddAuthentication(options =>
          {
              options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
              options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
          })
          .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
          .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
          {
              options.Authority = "https://demo.identityserver.io";
              options.ClientId = "interactive.confidential";
              options.ClientSecret = "secret";
              options.ResponseType = "code";
              options.SaveTokens = true;
              options.Scope.Add("api");
          });
      

      要编译上述配置,您应该添加 NuGet 包 Microsoft.AspNetCore.Authentication.CookiesMicrosoft.AspNetCore.Authentication.OpenIdConnect

      Startup.csConfigure 方法中,您需要在app.UseAuthorization(); 之前添加app.UseAuthentication();

      由于BearerTokenHandler 相当大,您可以从a GitHub repository 复制它。您需要IdentityModel 的 NuGet 包参考。
      此存储库还包含您要求的设置的完整工作示例。


      最后,您可能希望让用户能够退出。
      您可以通过在视图中添加链接来做到这一点:

      @if (User.Identity.IsAuthenticated)
      {
          <li class="nav-item">
              <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Logout">Logout</a>
          </li>
      }
      

      为此匹配的控制器方法:

      public IActionResult Logout()
      {
          return SignOut(
              CookieAuthenticationDefaults.AuthenticationScheme,
              OpenIdConnectDefaults.AuthenticationScheme);
      }
      

      应该是这样的。希望您能够跟随所有拼图。
      如果有不清楚的地方请告诉我。

      【讨论】:

        【解决方案4】:

        没有足够的声望给 IdentityServer4 的答案添加评论,所以我就在这里提一下。

        IS4 将不再免费用于商业用途: The Future of IdentityServer

        当前版本 (IdentityServer4 v4.x) 将是我们作为免费开源开发的最后一个版本。我们将继续支持 IdentityServer4,直到 2022 年 11 月 .NET Core 3.1 的生命周期结束。

        为了继续我们的工作,我们成立了一家新公司 Duende Software,IdentityServer4 将更名为 Duende IdentityServer。 Duende IdentityServer 将包含所有新功能工作,并将针对 .NET Core 3.1 和 .NET 5(以及更高版本)。

        此新产品将保持开源,但将提供双重许可(RPL 和商业)。如果您也在做免费的开源工作,RPL(互惠公共许可证)使 Duende IdentityServer 保持免费。如果您在商业场景中使用 Duende IdentityServer,则需要商业许可证。我们提供多种方式来许可 Duende IdentityServer,以适应不同的公司规模和使用模式。包含 RPL 许可证对我们很重要,因为它使我们能够认识开源社区和我们的贡献者并表达我们的感激之情。

        【讨论】:

          猜你喜欢
          • 2013-06-11
          • 1970-01-01
          • 2015-03-27
          • 2017-12-12
          • 2023-01-05
          • 2020-06-13
          • 2018-04-12
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多