将SSO OpenId / Azure AD身份验证添加到现有Web窗体应用程序

时间:2017-11-01 01:59:53

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

我有一个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中为响应添加一个伪auth票证,但这似乎没有改变任何东西 - 我仍然收到循环身份验证请求。

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);

2 个答案:

答案 0 :(得分:0)

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

我正在使用&#34;组织帐户&#34; (即O365电子邮件登录)我遇到了两个大问题(都解决了)。

第一期

间歇性地,当登录时会在两页之间来回进入无限重定向循环(这并不是一直发生的 - 只有在半小时的测试和登录和退出之后)。

如果我把它留下足够长的时间,它会说&#34;查询字符串太长&#34;。关于cookie和东西有很多冗长的解释,但我很难解决它。最后,它只是通过强制https而不是http

来解决

我不认为这是你的问题,因为它似乎每次都会这样做。也许读一读这个

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

一个答案是:

  

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

第二期

接下来就是:我们现有的授权系统位于我们数据库中的经典登录/ pwd表中(使用未加密的密码字段&gt;:|)。所以我需要拿起登录电子邮件并将其与此表中定义的角色相匹配。感谢回答我问题的那个人,我做了什么:

Capturing login event so I can cache other user information

这个答案意味着我可以:

  1. 首次登录时从数据库中获取用户角色
  2. 将此角色保存在现有本机C#安全对象
  3. 最重要的是:在我的控制器方法中使用本机授权注释,而无需方法中的任何自定义代码
  4. 我认为这就是你所追求的但问题确实是:你如何目前存储角色?在数据库表中?在Active Directory中?在Azure活动目录中?

答案 1 :(得分:0)

所以希望它可以帮助别人 - 这就是我最终的结果。在web.config中,身份验证模式设置为“Forms”。我添加了以下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并正确处理它。如果有人对如何改进它有任何想法 - 我很乐意听到它们,但目前这确实有效。