【问题标题】:Adding SSO OpenId/Azure AD auth to an existing Web Forms app将 SSO OpenId/Azure AD 身份验证添加到现有 Web 窗体应用程序
【发布时间】:2018-04-13 07:32:30
【问题描述】:

我有一个 Web 表单应用程序,当前使用表单身份验证(或 LDAP,然后设置 FormsAuthenticationTicket cookie)。我需要将 SSO 添加到这个项目,我目前正在使用 OpenID/Azure AD 进行身份验证。我配置了以下 Startup.cs。

     public void Configuration(IAppBuilder app)
    { 
        string appId = "<id here>";
        string aadInstance = "https://login.microsoftonline.com/{0}";
        string tenant = "<tenant here>"; 
        string postLogoutRedirectUri = "https://localhost:21770/";
        string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);

 app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
            app.UseCookieAuthentication(new CookieAuthenticationOptions());

            app.UseOpenIdConnectAuthentication(
             new OpenIdConnectAuthenticationOptions
             {
                 ClientId = appId,
                 Authority = authority,
                 PostLogoutRedirectUri = postLogoutRedirectUri,
                 Notifications = new OpenIdConnectAuthenticationNotifications
                 {
                     SecurityTokenReceived = context =>
                     {
                         System.Diagnostics.Debug.WriteLine("SecurityTokenReceived");
                         return Task.FromResult(0);
                     },

                     SecurityTokenValidated = async n =>
                     {
                         var claims_to_exclude = new[]
                         {
                             "aud", "iss", "nbf", "exp", "nonce", "iat", "at_hash"
                         };

                         var claims_to_keep =
                             n.AuthenticationTicket.Identity.Claims 
                             .Where(x => false == claims_to_exclude.Contains(x.Type)).ToList();
                         claims_to_keep.Add(new Claim("id_token", n.ProtocolMessage.IdToken));

                         if (n.ProtocolMessage.AccessToken != null)
                         {
                             claims_to_keep.Add(new Claim("access_token", n.ProtocolMessage.AccessToken));

                             //var userInfoClient = new UserInfoClient(new Uri("https://localhost:44333/core/connect/userinfo"), n.ProtocolMessage.AccessToken);
                             //var userInfoResponse = await userInfoClient.GetAsync();
                             //var userInfoClaims = userInfoResponse.Claims
                             //    .Where(x => x.Item1 != "sub") // filter sub since we're already getting it from id_token
                             //    .Select(x => new Claim(x.Item1, x.Item2));
                             //claims_to_keep.AddRange(userInfoClaims);
                         }

                         var ci = new ClaimsIdentity(
                             n.AuthenticationTicket.Identity.AuthenticationType,
                             "name", "role");
                         ci.AddClaims(claims_to_keep);

                         n.AuthenticationTicket = new AuthenticationTicket(
                             ci, n.AuthenticationTicket.Properties
                         );
                     },
                     MessageReceived = context =>
                     {
                         System.Diagnostics.Debug.WriteLine("MessageReceived");
                         return Task.FromResult(0);
                     },
                     AuthorizationCodeReceived = context =>
                     {
                         System.Diagnostics.Debug.WriteLine("AuthorizationCodeReceived"); 
                         return Task.FromResult(0);
                     },
                     AuthenticationFailed = context =>
                     {
                         System.Diagnostics.Debug.WriteLine("AuthenticationFailed");
                         context.HandleResponse();
                         context.Response.Write(  context.Exception.Message);
                         return Task.FromResult(0);
                     }
                     ,
                     RedirectToIdentityProvider = (context) =>
                     {
                         System.Diagnostics.Debug.WriteLine("RedirectToIdentityProvider"); 
                         //string currentUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.Path;
                         //context.ProtocolMessage.RedirectUri = currentUrl;

                         return Task.FromResult(0);
                     }
                 }
             }); 
            app.UseStageMarker(PipelineStage.Authenticate);

        }

我已将此放置在我的主人的页面加载事件中(尽管它似乎从未受到打击 - 当我导航到需要身份验证的页面时,肯定是其他原因导致身份验证过程开始。)

   if (!Request.IsAuthenticated)
                {
                    HttpContext.Current.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/Login.aspx" }, OpenIdConnectAuthenticationDefaults.AuthenticationType);
                }

我的 Azure 设置都是正确的,因为我正在使用 SecurityTokenValidated 和 AuthorizationCodeReceived 函数 - 我可以在索赔信息中看到我登录时使用的电子邮件,但我不确定下一步该做什么。因为我有一个永无止境的身份验证请求循环。我假设这是因为我没有将收到的索赔信息转换回表单身份验证?我试图在 AuthorizationCodeReceived 的响应中添加一个虚拟身份验证票,但这似乎没有改变任何东西 - 我仍然收到循环身份验证请求。

FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1, "<UserName>", DateTime.Now, DateTime.Now.AddMinutes(60), true,"");
String encryptedTicket = FormsAuthentication.Encrypt(authTicket); 
context.Response.Cookies.Append(FormsAuthentication.FormsCookieName, encryptedTicket);

【问题讨论】:

  • 您的问题是“永无止境的身份验证请求循环”吗?从记忆中,我很确定我不需要使用任何显式的登录代码——这一切都是通过将正确的东西添加到 startup.cs 来处理的。也许最好的解决方案是使用 OpenID 创建一个全新的项目并查看它自动生成的代码。
  • 问题有两个方面 - 永无止境的循环,以及我需要让登录 AD 的人与我认识的用户、他们在我的应用程序中被赋予的角色等相关的事实。(假设这将是一个直接的电子邮件地址比较)。不过我也会看一个新项目。

标签: c# azure single-sign-on openid-connect


【解决方案1】:

这不是一个明确的答案,但它太大了,无法评论。

我正在使用“组织帐户”(即 O365 电子邮件登录),我遇到了两个大问题(都已解决)。

第一期

间歇性地,登录时它会在两个页面之间来回进行无休止的重定向循环(这种情况并非一直发生 - 仅在半小时的测试和登录和注销之后)。

如果我把它留得足够长,它会说“查询字符串太长”。关于 cookie 和其他东西有很多冗长的解释,但我很难解决。最后它只是通过强制https而不是http来解决

我认为这不是您的问题,因为它似乎每次都会发生。或许可以通读一遍

New Asp.Net MVC5 project produces an infinite loop to login page

一个回答说:

不要调用受保护的 Web API(任何需要 授权)来自授权页面,例如 ~/Account/Login (它本身不会这样做。)。如果你这样做,你将进入 服务器端的无限重定向循环。

第二期

接下来的事情是:我们现有的授权系统位于我们数据库中的经典登录/密码表中(带有未加密的密码字段 >:| )。所以我需要获取登录电子邮件并将其与此表中定义的角色相匹配。感谢回答我问题的人:

Capturing login event so I can cache other user information

这个答案意味着我可以:

  1. 在初次登录时从数据库中获取用户角色
  2. 将此角色保存在现有的本机 C# 安全对象中
  3. 最重要的是:在我的控制器方法中使用本机授权注释,方法中没有任何自定义代码

我认为这就是您所追求的,但问题是:您当前如何存储角色?在数据库表中?在活动目录中?在 Azure 活动目录中?

【讨论】:

  • 用户/角色当前存储在应用程序数据库的 aspnet_Users/aspnet_UsersInRoles/aspnet_Roles 表中。然后,该应用程序使用 System.Web.Security.Membership.GetUser() 和 System.Web.Security.Roles.IsUserInRole("") 在各个页面中执行其工作。如果我能提供帮助,我真的不想碰任何这些,因此试图让经过身份验证的用户和我的应用知道的用户快乐:)
  • 因此,如果我理解正确,您有一个使用表单安全性的现有 ASP.Net 应用程序(这些表就是这样),并且您想将其更改为使用组织安全性(O365 登录),但您想要重用用户数据库中已有的所有现有角色定义。您也不想更改任何现有代码。
  • 这对我来说是全新的,所以我认为是的。从长远来看,我希望必须添加对其他身份提供者的支持——天蓝色目录身份验证只是他们想要添加到系统中的第一个选项。 Forms 安全性永远不会在我的应用程序中完全消失 - 总会有系统用户没有登录 Azure 或任何其他来源,所以我真的只是在寻找最简单的方法让他们一起玩得很好.
  • 参考这个页面:stackoverflow.com/questions/38999304/… 我有一个非常基本的请求/响应/登录,根本没有使用 owin 库。在此尝试中,我手动构造了 login.microsoftonline.com 的 GET 请求,解析了返回的令牌,然后根据问题构造了 FormsAuthenticationTicket 对象,然后重定向到我的安全页面。这似乎确实有效 - 但不确定这是否是最好的方法?
  • 我有点不知所措,但我认为您应该能够在不执行任何手动过程的情况下获得一个填充了用户 ID 的“安全对象”.....虽然不确定
【解决方案2】:

所以希望它可以帮助其他人 - 这就是我最终的结果。在 web.config 中,身份验证模式设置为“表单”。我添加了以下 Startup.cs

  public class Startup
    {
        public void Configuration(IAppBuilder app)
        {

        var appId = ConfigurationCache.GetConfigurationString(TOS_Configuration.KEY_SSO_APPID);
        var authority = ConfigurationCache.GetConfigurationString(TOS_Configuration.KEY_SSO_AUTHORITY);

        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
        app.UseCookieAuthentication(new CookieAuthenticationOptions());

        app.UseOpenIdConnectAuthentication(
         new OpenIdConnectAuthenticationOptions
         {
             ClientId = appId,
             Authority = authority,
             Notifications = new OpenIdConnectAuthenticationNotifications
             {
                 AuthorizationCodeReceived = context =>
                 {

                     string username = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value; 

                     FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddMinutes(60), true, "");
                     String encryptedTicket = FormsAuthentication.Encrypt(authTicket);
                     context.Response.Cookies.Append(FormsAuthentication.FormsCookieName, encryptedTicket);

                     return Task.FromResult(0);
                 },
                 AuthenticationFailed = context =>
                 {
                     context.HandleResponse();
                     context.Response.Write(context.Exception.Message);
                     return Task.FromResult(0);
                 }
             }
         });

        // This makes any middleware defined above this line run before the Authorization rule is applied in web.config
        app.UseStageMarker(PipelineStage.Authenticate);

    }

}

我没有向我的站点母版页添加任何质询,而是将以下内容添加到我的登录页面以触发身份验证质询:

if (!Request.IsAuthenticated && AttemptSSO)
{
    ReturnURL = Request.QueryString["ReturnUrl"];
    HttpContext.Current.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/Login.aspx" }, OpenIdConnectAuthenticationDefaults.AuthenticationType);
 }
 else if (Request.IsAuthenticated && AttemptSSO)
 {
     if (!string.IsNullOrEmpty(ReturnURL))
     {
           var url = ReturnURL;
           ReturnURL = "";
           Response.Redirect(ResolveUrl(url));
     }
     else
     {
            Response.Redirect(ResolveUrl("~/Default.aspx"));
     }
 }

这意味着如果用户在没有有效表单身份验证令牌的情况下到达经过身份验证的页面,他们将被重定向到登录页面。登录页面负责决定是否设置了 SSO 并对其进行适当处理。如果有人对如何改进它有任何想法 - 我很想听听他们的意见,但目前这确实有效。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-06-27
    • 2015-12-22
    • 2019-05-28
    • 2022-10-25
    • 2018-03-05
    • 1970-01-01
    • 2020-07-14
    相关资源
    最近更新 更多