【问题标题】:MVC Application using WebApi, Identity, and using Cookies safely使用 WebApi、身份和安全使用 Cookie 的 MVC 应用程序
【发布时间】:2016-05-10 21:34:28
【问题描述】:

我需要让我的启用 Identity 的 MVC 站点与 WebApi 使用 cookie 来对站点和 Web api 进行身份验证。处理过此问题的任何人都可能知道这很容易受到 XSS 攻击,因为常规登录 cookie 可能会通过访问恶意页面发送到您的 webapi 方法。

在 web api 中使用 cookie 的奇怪要求是问题的根源。有什么方法可以安全地做到这一点?

我有一个在AuthorizationFilter 中使用表单身份验证的解决方案(在下面发布),但我希望能够利用身份框架的功能,例如随处声明和注销。

using System;
using System.Web.Http.Filters;
using System.Web.Http;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
using System.Security.Principal;
using System.Web;
using System.Web.Security;
using System.Collections.Generic;

namespace Filters
{

    /// <summary>
    /// An authentication filter that uses forms authentication cookies.
    /// </summary>
    /// <remarks>Use the *Cookie static methods to manipulate the cookie on the client</remarks>
    public class FormsAuthenticationFilter : Attribute, IAuthenticationFilter
    {

        public static long Timeout { get; set; }
        public static string CookieName { get; set; }

        public FormsAuthenticationFilter()
        {
            // Default Values
            FormsAuthenticationFilter.Timeout = FormsAuthentication.Timeout.Minutes;
            FormsAuthenticationFilter.CookieName = "WebApi";
        }


        public FormsAuthenticationFilter(long Timeout, string CookieName)
        {
            FormsAuthenticationFilter.Timeout = Timeout;
            FormsAuthenticationFilter.CookieName = CookieName;

        }

        public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
        {

            HttpRequestMessage request = context.Request;

            // Get cookie
            HttpCookie cookie = HttpContext.Current.Request.Cookies[FormsAuthenticationFilter.CookieName];

            //If no cookie then do nothing
            if (cookie == null)
            {
                return Task.FromResult(0);
            }

            //If empty cookie then raise error
            if (String.IsNullOrEmpty(cookie.Value))
            {
                context.ErrorResult = new AuthenticationFailureResult("Empty ticket", request);
                return Task.FromResult(0);
            }

            //Decrypt ticket
            FormsAuthenticationTicket authTicket = default(FormsAuthenticationTicket);

            try
            {
                authTicket = FormsAuthentication.Decrypt(cookie.Value);

            }
            catch (Exception)
            {
                context.ErrorResult = new AuthenticationFailureResult("Invalid ticket", request);
                return Task.FromResult(0);

            }

            //Check if expired

            if (authTicket.Expired)
            {
                context.ErrorResult = new AuthenticationFailureResult("Ticket expired", request);
                return Task.FromResult(0);

            }

            //If caching roles in userData field then extract
            string[] roles = authTicket.UserData.Split(new char[] { '|' });

            // Create the IIdentity instance
            IIdentity id = new FormsIdentity(authTicket);

            // Create the IPrinciple instance
            IPrincipal principal = new GenericPrincipal(id, roles);

            // Set the context user 
            context.Principal = principal;

            // Update ticket if needed (sliding window expiration)
            if ((authTicket.Expiration - DateTime.Now).TotalMinutes < (FormsAuthenticationFilter.Timeout / 2))
            {
                RenewCookie(authTicket);

            }

            return Task.FromResult(0);

        }

        public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
        {

            //Do nothing
            return Task.FromResult(0);

        }

        public bool AllowMultiple
        {
            get { return false; }
        }

        /// <summary>
        /// Renews the cookie on the client using the specified FormsAuthenticationTicket
        /// </summary>
        /// <param name="OldTicket">A still-valid but aging FormsAuthenticationTicket that should be renewed</param>
        /// <remarks></remarks>
        protected static void RenewCookie(FormsAuthenticationTicket OldTicket)
        {
            HttpContext.Current.Response.Cookies.Add(GetCookie(OldTicket.Name, OldTicket.UserData));

        }

        /// <summary>
        /// Sets the authentication cookie on the client
        /// </summary>
        /// <param name="UserName">The username to set the cookie for</param>
        /// <remarks></remarks>
        public static void SetCookie(String user, IList<string> roles)
        {

            HttpContext.Current.Response.Cookies.Add(GetCookie(user, string.Join("|", roles)));

        }

        /// <summary>
        /// Removes the authentication cookie on the client
        /// </summary>
        /// <remarks>Cookie is removed by setting the expires property to in the past, may not work on all clients</remarks>
        public static void RemoveCookie()
        {
            if ((HttpContext.Current.Response.Cookies[FormsAuthenticationFilter.CookieName] != null))
            {
                HttpContext.Current.Response.Cookies[FormsAuthenticationFilter.CookieName].Expires = DateTime.Now.AddDays(-1);
            }

        }

        private static HttpCookie GetCookie(string UserName, string UserData)
        {

            //Create forms auth ticket
            FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, UserName, DateTime.Now, DateTime.Now.AddMinutes(FormsAuthenticationFilter.Timeout), false, UserData);

            //Create cookie with encrypted contents
            HttpCookie cookie = new HttpCookie(FormsAuthenticationFilter.CookieName, FormsAuthentication.Encrypt(ticket));
            cookie.Expires = DateTime.Now.AddMinutes(FormsAuthenticationFilter.Timeout);

            //Return it
            return cookie;

        }

        protected class AuthenticationFailureResult : IHttpActionResult
        {

            public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request)
            {
                this.ReasonPhrase = reasonPhrase;
                this.Request = request;
            }

            public string ReasonPhrase { get; set; }
            public HttpRequestMessage Request { get; set; }

            public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
            {
                return Task.FromResult(Execute());
            }

            private HttpResponseMessage Execute()
            {
                HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
                response.RequestMessage = Request;
                response.ReasonPhrase = ReasonPhrase;
                return response;
            }

        }

    }

}

【问题讨论】:

    标签: asp.net asp.net-mvc cookies asp.net-web-api asp.net-identity


    【解决方案1】:

    让我先说这只是一个解决方案,如果 WebApi 方法永远不会通过您网站上的 javascript 调用。仅当您计划从其他客户端(例如移动应用程序)使用 WebApi 时才应使用此选项。

    解决方案在于为 WebApi 使用单独的 cookie。 WebApi 会拒绝正常的站点授权 cookie,只使用自己的。

    在您的WebApiConfig 类中添加一个常量和一个静态方法来配置cookie 中间件:

    public const string WebApiCookieAuthenticationType = "WebApiCookie";
    
    public static CookieAuthenticationOptions CreateCookieAuthenticationOptions()
    {
    
         return new CookieAuthenticationOptions
         {
            AuthenticationType = WebApiConfig.WebApiCookieAuthenticationType,
            AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Passive,
            CookieName = ".AspNet.WebApiCookie",
            CookiePath = "/api",
            ExpireTimeSpan = TimeSpan.FromDays(14),
            SlidingExpiration = true,
            LoginPath = new PathString("/api/auth"),
            Provider = new CookieAuthenticationProvider
            {
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromMinutes(60),
                    regenerateIdentity: (manager, user) => manager.CreateIdentityAsync(user, WebApiConfig.WebApiCookieAuthenticationType)
                ),
                 OnApplyRedirect = ctx => {}
            }
        };
    
    }
    

    注意AuthenticationTypeAuthenticationMode,它们会在我们注册后防止它干扰正常的站点cookies。我们接下来在您的 Startup 类中执行此操作,将其放置在现有的 app.UseCookieAuthentication() 调用下方:

    // Setup WebApi Cookie Authentication
    app.UseCookieAuthentication(WebApiConfig.CreateCookieAuthenticationOptions());
    

    我们还需要配置 WebApi 以忽略正常的站点 cookie 并处理我们的特殊 cookie。在您的 WebApiConfig.Register() 方法中执行此操作:

    // Ignore website auth
    config.SuppressHostPrincipal();
    config.Filters.Add(new HostAuthenticationFilter(WebApiCookieAuthenticationType));
    

    现在我们已经设置好了,但是你如何发布 auth cookie?在您的 webapi 中创建一个授权控制器方法,该方法使用标准身份 SignInManager 但有一个额外的步骤:

    [AllowAnonymous]
    public async Task<IHttpActionResult> Authenticate(string userName, string password)
    {
    
        // Get signIn manager
        var signInManager = HttpContext.Current.Request.GetOwinContext().Get<ApplicationSignInManager>();
        if (signInManager == null)
        {
            return BadRequest();
        }
        // Important!
        signInManager.AuthenticationType = WebApiConfig.WebApiCookieAuthenticationType;
    
        // Sign In
        var result = await signInManager.PasswordSignInAsync(userName, password, false, shouldLockout: true);
        if (result == SignInStatus.Success)
        {
    
            // Return
            return Ok();
    
        }
    
        // Failure
        return BadRequest();
    
    }
    

    特别注意我们设置SignInManagerAuthenticationType的位置,这将使它使用我们的特殊WebApi cookie而不是标准站点cookie。

    陷阱

    一个问题是默认站点模板使用对DefaultAuthenticationTypes.ApplicationCookie 身份验证类型的硬编码引用来覆盖SignInManagerCreateUserIdentityAsync 方法。这将干扰此解决方案。要修复它,您可以将覆盖更改为:

    public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
    {
        if (this.AuthenticationType == DefaultAuthenticationTypes.ApplicationCookie)
        {
            return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
        }
        else
        {
            return base.CreateUserIdentityAsync(user);
        }
    
    }
    

    另一个问题是压缩。如果您使用出色的 Microsoft.AspNet.WebApi.Extensions.Compression 包,您将需要禁用 Web api 方法的压缩,使用 SignInManager 登录/注销,因为它在 Identity 可以将 Set-Cookie 标头添加到响应之前写入响应。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-03-05
      • 1970-01-01
      • 2020-06-16
      • 2011-10-06
      • 1970-01-01
      • 1970-01-01
      • 2017-08-15
      • 2012-11-27
      相关资源
      最近更新 更多