Authentication of Web APi: Two Implementations [2] (13)

Keywords: ASP.NET encoding Attribute IIS Windows

Preface

In the previous section, we explained authentication and its basic information in detail. In this section, we implemented authentication in two different ways, and analyzed how to make rational use of these two ways. The basic knowledge involved in this article, please refer to the previous article, and stop talking nonsense.

Preface

For so-called authentication, in the final analysis, there are many ways to achieve security in the Web API, [accepted] way to deal with IIS-based security (through the Windows Identity mentioned in the previous section depends on HttpContext and IIS authentication) or to use the message processing mechanism in the Web API, but if we want the application to run outside IIS, then Windows I Denitity seems impossible. At the same time, the Web API itself does not provide a direct way to deal with authentication. We have to customize to achieve authentication function. At the same time, this is also the recommended way, do it yourself, and eat plenty of food and clothes.

Warm Tip: The following implementations are based on Basic authentication. If you are not familiar with Basic authentication in Http protocol, please refer to this article first.[ Gardener Seabird - Introduction to Basic Basic Basic Certification and Digest Summary Certification].

 

In any case, for our applications, we need to use Credential-based user authentication in the business layer, because it is the requirement of the client side, so the client needs to be clear about basic authentication. Basic authentication is very simple and supports any Web client, but the disadvantage of basic authentication is unsafe. It can be encrypted by using SSL. To a certain extent, security is guaranteed. It can also be said that if the general application is only encoding and not encrypted through basic authentication. Let's take a look at the pictures given in the previous section.

From the rough information of the above pictures, we can see that there is a Web API message processing pipeline between the request and the Action method, and HttpMessageHandler and authentication filter before the request to the target element, so we can customize the two to achieve authentication. Now let's look at it one by one.

Authorization Filter Attribute Based on Web API

First step

We customize a class of authenticated identity (username and password), which must also be inherited from GenericIdentity. Since it is based on basic authentication, the type is, of course, Basic.

    public class BasicAuthenticationIdentity : GenericIdentity
    {
        public string Password { get; set; }
        public BasicAuthenticationIdentity(string name, string password)
            : base(name, "Basic")
        {
            this.Password = password;
        }
    }

The second step

We need to customize an authentication filter feature and inherit the Authorization FilterAttribute, which becomes as follows:

    public class BasicAuthenticationFilter : AuthorizationFilterAttribute
    {
        public override void OnAuthorization(HttpActionContext actionContext)
        {}
    }

So what should we write in this rewriting method? Let's analyze it slowly. Look down, please.

  • Parse request header

Firstly, we need to get the request header for the request sent by the client, and then parse the Authorization in the request header. If the parameter is empty at this time, we will return to the client and issue a query.

            string authParameter = null;

            var authValue = actionContext.Request.Headers.Authorization;  //actionContext: Action method request context if (authValue != null && authValue.Scheme == "Basic")
                authParameter = authValue.Parameter;  //authparameter: Gets the Base64-encoded (user: password) in the request if (string.IsNullOrEmpty(authParameter))

                return null;

Secondly, if the parameters in the authentication are not empty at this time and begin to decode them, and return a Basic Authentication Identity object, if the object is empty at this time, then return to the client, and issue a query.

           authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter)); //Decoding the coded parameters var authToken = authParameter.Split(':');  //The decoded parameter format is (username: password) to segment it. if (authToken.Length < 2)
                return null;

            return new BasicAuthenticationIdentity(authToken[0], authToken[1]); //Pass the partitioned username and password to this constructor for initialization

Finally, we encapsulate the above two as a ParseHeader method for invocation.

        public virtual BasicAuthenticationIdentity ParseHeader(HttpActionContext actionContext)
        {
            string authParameter = null;

            var authValue = actionContext.Request.Headers.Authorization;
            if (authValue != null && authValue.Scheme == "Basic")
                authParameter = authValue.Parameter;

            if (string.IsNullOrEmpty(authParameter))

                return null;

            authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter));

            var authToken = authParameter.Split(':');
            if (authToken.Length < 2)
                return null;

            return new BasicAuthenticationIdentity(authToken[0], authToken[1]);
        }
  • Next, we encapsulate the failure of authentication and need to initiate an authentication challenge as a method, Challenge.

        void Challenge(HttpActionContext actionContext)
        {
            var host = actionContext.Request.RequestUri.DnsSafeHost;
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
            actionContext.Response.Headers.Add("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", host));
           
        } 
  • Define a method to verify usernames and passwords, and modify it as a virtual method, so as not to add other relevant user data in the future.

        public virtual bool OnAuthorize(string userName, string userPassword, HttpActionContext actionContext)
        {
            if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(userPassword))

                return false;
            else
                return true;

        }
  • Set the authentication identity to the Principal property in the current thread after the authentication is successful

           var principal = new GenericPrincipal(identity, null);

            Thread.CurrentPrincipal = principal;

            //The following is for ASP.NET Set up
            //if (HttpContext.Current != null)
            //    HttpContext.Current.User = principal;

The third step

Everything is ready, and then the corresponding call in the rewrite method can be made, as follows:

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
    public class BasicAuthenticationFilter : AuthorizationFilterAttribute
    {
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            var userIdentity = ParseHeader(actionContext);
            if (userIdentity == null)
            {
                Challenge(actionContext);
                return;
            }

            if (!OnAuthorize(userIdentity.Name, userIdentity.Password, actionContext))
            {
                Challenge(actionContext);
                return;
            }

            var principal = new GenericPrincipal(userIdentity, null);

            Thread.CurrentPrincipal = principal;

            base.OnAuthorization(actionContext);
        }

The fourth step

Custom Basic Authentication Filter is customized and inherited from Basic Authentication Filter to override its virtual method.

    public class CustomBasicAuthenticationFilter : BasicAuthenticationFilter
    {
        public override bool OnAuthorize(string userName, string userPassword, HttpActionContext actionContext)
        {
            if (userName == "xpy0928" && userPassword == "cnblogs")

                return true;
            else
                return false;

        }
    }

Last step

Register and invoke custom authentication features

    config.Filters.Add(new CustomBasicAuthenticationFilter());

    [CustomBasicAuthenticationFilter]
    public class ProductController : ApiController
    {....}

So far, the authentication method has been fully realized, and then we will check and accept our results through Sogob Browser.

Seeing the following pictures of authenticating its username and password, we know that we're half as successful.

We click Cancel to see if we return to 401 and add the question header, WWW-Authenticate, as we expected.

We enter the correct username and password and try again. The result is that the authentication is successful, as follows:

Web API-based message processing pipeline (HttpMessageHandler) for authentication

We know that HttpMessageHandler plays an important role in the request-response message processing pipeline in the Web API, but the real way to connect pipelines in series is Delegating Handler. If you don't understand the Web API message pipeline, please refer to the previous series of articles, so we can customize the pipeline to intercept by inheriting Delegating Handler. Next we step by step to achieve authentication based on this pipeline.

First step

Consistent with the first method, it is no longer described.

The second step

This step, of course, is to customize the pipeline to process and inherit Delegating Handler, overload the SendAsync method in this class, and respond by getting its request and processing. If you don't understand the specific implementation in this class, please refer to the previous series of articles.

  • Similarly, we need to parse the request header according to the request. We still need to parse the header method, but we need to modify it slightly.

        public virtual BasicAuthenticationIdentity ParseHeader(HttpRequestMessage requestMessage)
        {
            string authParameter = null;

            var authValue = requestMessage.Headers.Authorization;
            if (authValue != null && authValue.Scheme == "Basic")
                authParameter = authValue.Parameter;

            if (string.IsNullOrEmpty(authParameter))

                return null;

            authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter));

            var authToken = authParameter.Split(':');
            if (authToken.Length < 2)
                return null;

            return new BasicAuthenticationIdentity(authToken[0], authToken[1]);
        }
  • At this time, the query has to be modified accordingly, because it no longer depends on the context of the Action request, but on the request (HttpRequestMessage) and response (HttpResponseMessage).

        void Challenge(HttpRequestMessage request,HttpResponseMessage response)
        {
            var host = request.RequestUri.DnsSafeHost;

            response.Headers.Add(authenticationHeader, string.Format("Basic realm=\"{0}\"", host));

        }
  • The code that eventually inherits from Delegating Handler is as follows

    public class BasicAuthenticationHandler : DelegatingHandler
    {
        private const string authenticationHeader = "WWW-Authenticate";
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var crendentials = ParseHeader(request);

            if (crendentials != null)
            {
                var identity = new BasicAuthenticationIdentity(crendentials.Name, crendentials.Password);

                var principal = new GenericPrincipal(identity, null);

                Thread.CurrentPrincipal = principal;

                //Aim at ASP.NET Set up
                //if (HttpContext.Current != null)
                //    HttpContext.Current.User = principal;
            }

            return base.SendAsync(request, cancellationToken).ContinueWith(task => {
                var response = task.Result;
                if (crendentials == null && response.StatusCode == HttpStatusCode.Unauthorized)
                {
                    Challenge(request, response);
                }

                return response;
            });



        }

        void Challenge(HttpRequestMessage request,HttpResponseMessage response)
        {
            var host = request.RequestUri.DnsSafeHost;

            response.Headers.Add(authenticationHeader, string.Format("Basic realm=\"{0}\"", host));

        }

        public virtual BasicAuthenticationIdentity ParseHeader(HttpRequestMessage requestMessage)
        {
            string authParameter = null;

            var authValue = requestMessage.Headers.Authorization;
            if (authValue != null && authValue.Scheme == "Basic")
                authParameter = authValue.Parameter;

            if (string.IsNullOrEmpty(authParameter))

                return null;

            authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter));

            var authToken = authParameter.Split(':');
            if (authToken.Length < 2)
                return null;

            return new BasicAuthenticationIdentity(authToken[0], authToken[1]);
        }
    }

The third step

Our custom Basic Authentication Filter has to inherit the Authorization Attribute, which is also inherited from the Authorization Filter Attribute. We need to use the IsAuthorized method in Authorization Attribute to verify whether the Principal in the current thread has been authorized.

    public class BasicAuthenticationFilter : AuthorizeAttribute
    {
        protected override bool IsAuthorized(HttpActionContext actionContext)
        {

            var identity = Thread.CurrentPrincipal.Identity;
            if (identity != null && HttpContext.Current != null)
                identity = HttpContext.Current.User.Identity;

            if (identity != null && identity.IsAuthenticated)
            {

                var basicAuthIdentity = identity as BasicAuthenticationIdentity;
                 
//Adding additional business logic validation code as needed
if (basicAuthIdentity.Name == "xpy0928" && basicAuthIdentity.Password == "cnblogs") { return true; } } return false; } }

From the return value of IsAuthorized method, if false, 401 status code will be returned, which will trigger the query in Basic Authentication Handler, and in this method we need to add the business logic code of authenticated users. At the same time, we also said that the filter feature of our first custom implementation is AuthorizationFilterAttribute (if we have more logic to use this feature is a good choice), and here is AuthorizeAttribute (for authenticating users and returning bool values to use this filter feature is a good choice).

The fourth step

Register custom pipes and authentication filter features

            config.MessageHandlers.Add(new BasicAuthenticationHandler());
            config.Filters.Add(new BasicAuthenticationFilter());

Last step

    [BasicAuthenticationFilter]
    public class ProductController : ApiController
    {.....}

Next we check the results through 360 Extreme Speed Browser. Click on the button to request the controller directly

Next cancel and return 401

This is the perfect end.

Sum up

Is it a problem to implement authentication with Authorization Filter Attribute or HttpMessageHandler?

By comparing the implementation of the two methods, there is a significant difference. Personally, AuthorizationFilterAttribute is simpler and more compact to implement authentication, because every implementation is everywhere. In most scenarios of custom login, it is more efficient to use HttpMessage for such compact business logic with filters. The advantage of Handler is that it is a global application and part of the Web API message processing pipeline. HttpMessageHandler works better if different authentication is used for different parts, but at this point you need to customize a filter, especially when MessageHandler needs a filter for an authentication. So in summary, we should choose the corresponding way to achieve authentication according to different application scenarios.

 

Source code links

 Web API Authentication. 7z

Reference page:

http://www.yuanjiaocheng.net/webapi/mvc-consume-webapi-delete.html

http://www.yuanjiaocheng.net/entity/dbset-class.html

http://www.yuanjiaocheng.net/entity/setenvrionment.html

http://www.yuanjiaocheng.net/webapi/create-crud-api-1.html

http://www.yuanjiaocheng.net/webapi/media-formatter.html

http://www.yuanjiaocheng.net/Linq/why-linq.html

http://www.yuanjiaocheng.net/CSharp/csharp-nullable.html

http://www.yuanjiaocheng.net/ASPNET-CORE/core-configuration.html

http://www.yuanjiaocheng.net/mvc/mvc-controller.html

http://www.yuanjiaocheng.net/mvc/viewdata-in-asp.net-mvc.html

http://www.yuanjiaocheng.net/mvc/mvc-helper-Label.html

Posted by stallingjohn on Sun, 31 Mar 2019 07:42:29 -0700