User authentication and authorization

In the last article, we added view engine support to output truly dynamic pages. With Controller support, application developers are now free to execute business logic and output desired page effects, and a true Web server is basically in place. However, most business systems also require user Authentication and Authorization functions that allow a user to log in and out of the system and determine what he or she can do based on the user’s permissions.

We do not consider the implementation of user Authorization here. Because there are great differences in the processing of authorization mechanism and the division of resources among different systems, it is not suitable to be implemented at the bottom level, and it is more reasonable to leave the decision to the business layer. On the other hand, user login/logout is almost the basic function of any system, the function will not have much change, if also put into the business layer to achieve, it means that each system needs to be repeated again, which is obviously a waste of development resources. In the previous article, we implemented Session support. Now we can add the ability to handle users on top of sessions.

As mentioned in the Middleware section of this article, the Web pipeline is made up of a series of Middleware components, each of which should implement a relatively independent function. The Middleware components should be designed to be independent of each other to minimize coupling and avoid introducing hidden errors. However, some middleware components still rely on each other, such as sessions. Without sessions, user authentication (and other application functions) would not be possible. This is a problem to be aware of when configuring middleware (if well designed, consider throwing an error message whenever a Session is found unavailable for the vendor to troubleshoot).

Most Web applications that include social features also include third-party login mechanisms (based on OpenID or OAuth). However, the login process of OpenID/OAuth is very different from the authentication mechanism based on the server itself, which can basically be regarded as two completely different login methods. Therefore, some design methods will separate the login mechanism from the server itself and make the same service interface based on OpenID/OAuth, so that the login process can be relatively unified with the social login method (of course, there are still some differences in the interface). Since the difficulty of third-party login mainly lies in understanding the protocol and has little to do with the Web server itself, we will not consider it here.

Here’s a little digression. In previous articles, I tried to mimic the ASP.NET MVC interface whenever possible to bring the reader closer to a real server implementation. But I won’t try to mimic the framework’s interface in this article. ASP.NET MVC recently introduced mechanisms such as Identity are, in my opinion, overly complex. Microsoft’s intention may be to design a powerful architecture that covers everything, but for most ordinary Web applications with relatively simple requirements, it is suspected of being over-designed and not easy to use, and the so-called flexibility may not be as high as the designers intended. Of course, this is my personal opinion, and you can have a different opinion.

code

The sample code of this article has been put on Github, and the code associated with each article is placed in a separate branch for readers’ reference. Therefore, to get the sample code for this article, use the following command:

git clone https://github.com/shuhari/web-server-succinctly-example.git
git checkout -b  06-authentication origin/06-authentication
Copy the code

implementation

We’ve implemented sessions, and adding user information to them is easy (if security is not a concern). However, in order for the user to log in, we also have to handle POST requests from the Web server, and HttpListenerRequest doesn’t provide us with this interface. Therefore, we also need a piece of middleware to handle POST requests, which I call BodyParser (a name borrowed from NodeJs/Express).

HttpListenerRequest does not contain the form information that was posted, so it is first wrapped again:

public class HttpServerRequest { public HttpServerRequest(HttpListenerRequest request) { _innerRequest = request; Form = new NameValueCollection(); } private readonly HttpListenerRequest _innerRequest; public CookieCollection Cookies => _innerRequest.Cookies; public Uri Url => _innerRequest.Url; public string HttpMethod => _innerRequest.HttpMethod; public IPEndPoint RemoteEndPoint => _innerRequest.RemoteEndPoint; public Stream InputStream => _innerRequest.InputStream; public NameValueCollection Form { get; private set; }}Copy the code

This is a very simple encapsulation — most attributes just re-expose functionality already provided. But the Form is new and contains the submitted Form information.

Next, the HttpServerContext is also slightly modified to return the wrapped request:

public class HttpServerContext { public HttpServerContext(HttpListenerContext context) { _innerContext = context; Request = new HttpServerRequest(context.Request); } private readonly HttpListenerContext _innerContext; public HttpServerRequest Request { get; private set; }... }Copy the code

Properties are ready to implement middleware:

public class BodyParser : IMiddleware { public MiddlewareResult Execute(HttpServerContext context) { var request = context.Request; if (request.HttpMethod.Equals("POST", StringComparison.InvariantCultureIgnoreCase)) { using (var reader = new StreamReader(request.InputStream, Encoding.UTF8)) { string postData = reader.ReadToEnd(); foreach (var kv in postData.Split('&')) { int index = kv.IndexOf('='); if (index > 0) { string key = kv.Substring(0, index); string value = HttpUtility.UrlDecode(kv.Substring(index + 1)); request.Form[key] = value; } } } } return MiddlewareResult.Continue; }}Copy the code

The middleware here implements normal POST requests, but does not handle more complex formats such as multipart/ FormData. If file uploads are to be implemented, then you need to do a bit more work according to the protocol format.

In order to access the current user in the controller and view, let’s add a few more properties. In addition, when the user logs in successfully, the page needs to be redirected, so add a subclass of ActionResult (RedirectResult) :

public abstract class Controller : IController { public HttpServerContext HttpContext { get; internal set; } protected ISession Session => HttpContext.Session; protected IPrincipal User => HttpContext.User; protected HttpServerRequest Request => HttpContext.Request; protected RedirectResult Redirect(string url) { return new RedirectResult(url); } } public class RedirectResult : ActionResult { public RedirectResult(string url) { _url = url; } private readonly string _url; public override void Execute(HttpServerContext context) { context.Response.StatusCode = 301; context.Response.AddHeader("Location", _url); context.Response.OutputStream.Close(); }}Copy the code

We originally declared a counter in the controller to check whether the Session is working properly. Now this information is not important. Accordingly, we need to access the current user:

public class HomeController : Controller { public ActionResult Index() { ... var model = new { title = "Homepage", user = User, }; return View("Index", model); }}Copy the code

In the view, we check the current user. If not already logged in, the login form is displayed; Otherwise, the logout button is displayed.

    @if (Model.user.Identity.IsAuthenticated)
    {
        <a href="/Home/Logout">Logout</a>
    }
    else
    {
        <form method="post" action="/Home/Login">
            <div>
                <label for="username">User name:</label>
                <input type="text" name="username"/>
            </div>
            <div>
                <label for="password">Password:</label>
                <input type="password" name="password"/>
            </div>
            <div>
                <button type="submit">Login</button>
            </div>
        </form>
    }
</html>
Copy the code

The Login method extracts the Login information from the form. If the Login succeeds, the user is redirected. Otherwise return to the original form. The Logout method logs out the current user:

public class HomeController : Controller { public ActionResult Login() { string userName = Request.Form["username"]; string password = Request.Form["password"]; if (userName == "admin" && password == "1234") { Authentication.Login(HttpContext, userName); } return Redirect("/Home/Index"); } public ActionResult Logout() { Authentication.Logout(HttpContext); return Redirect("/Home/Index"); }}Copy the code

Of course, there is also Authentication logic that needs to be implemented. To record the current user, declare two more objects, one for the logged in user and one for the anonymous user (not logged in). This is a Null Object pattern: even if the User is not logged in, we can get a valid User Object without throwing an NPE. As an example, we don’t want to actually build a database to store users, so this is just a mock judgment of user information.

public class AnonymousUser : IPrincipal
{
    public bool IsInRole(string role)
    {
        return false;
    }

    public IIdentity Identity => new AnonymousIdentity();
}

class AnonymousIdentity : IIdentity
{
    public string Name => "Anonymouse";

    public string AuthenticationType => "";

    public bool IsAuthenticated => false;
}

public class User : IPrincipal
{
    public User(string name)
    {
        _name = name;
    }

    private readonly string _name;

    public bool IsInRole(string role)
    {
        return false;
    }

    public IIdentity Identity => new GenericIdentity(_name, "");
}
Copy the code

Then implement Authentication. This is also middleware and provides an interface to set or clear user information in the Session when the user logs in/out. In practice it may make more sense to put the interface and implementation in two classes, which are written together for simplicity:

public class Authentication : IMiddleware
{
    private const string _cookieName = "_userid_";

    private const string _sessionKey = "_user_";

    public MiddlewareResult Execute(HttpServerContext context)
    {
        IPrincipal user = null;
        var cookie = context.Request.Cookies[_cookieName];
        if (cookie != null)
        {
            user = context.Session[_sessionKey] as User;
        }
        user = user ?? new AnonymousUser();
        context.User = user;

        return MiddlewareResult.Continue;
    }

    public static void Login(HttpServerContext context, string userName)
    {
        var user = new User(userName);
        context.Session[_sessionKey] = user;
        context.User = user;

        var cookie = new Cookie(_cookieName, userName);
        context.Response.SetCookie(cookie);
    }

    public static void Logout(HttpServerContext context)
    {
        context.Session.Remove(_sessionKey);
        context.User = new AnonymousUser();

        var cookie = new Cookie(_cookieName, "");
        cookie.Expired = true;
        context.Response.SetCookie(cookie);
    }
}
Copy the code

Finally, configure the middleware. As mentioned earlier, Authentication must be added after Session:

class Program { .... static void RegisterMiddlewares(IWebServerBuilder builder) { builder.Use(new HttpLog()); / / builder. Use (new BlockIp (" : "1", "127.0.0.1")); builder.Use(new SessionManager()); builder.Use(new BodyParser()); builder.Use(new Authentication()); . }}Copy the code

All done! Now you can open your browser and log in and out (dead users, of course).

Afterword.

That concludes this article series. In principle, the Web server we have implemented already has almost all the features that a production server should have, although there are still many details that are not perfect. Code statistics (below) show that, excluding whitespace and comments, we have implemented the basic skeleton of a Web server in about 600 + lines of code (and a little HTML). I’m sure most readers won’t have a hard time writing this code (again, it’s not easy to get security right).

I’m sure most readers of this article won’t actually wank a server themselves. However, doing it yourself can help build your confidence ———— it doesn’t require magic skills, nor is it a feat only experienced professionals can accomplish, but it can be done by ordinary people (many well-known frameworks, including JUnit and ASP.NET MVC, are said, All in the short time it took to write them on the plane). In addition, you will have a better understanding of the existing framework, which will give you more confidence when you need to scale the server or troubleshoot a problem.

Finally, we’d like to thank Syncfusion for the free ebook and thank you for your patience.

series

  • Write your own Web server (index) in C#
  • Write a Web server in C#, part 1 – basics
  • Write a Web server in C#, part 2 – middleware
  • Write a Web server in C#, part 3 – routing
  • Write your own Web server in C#, part 4 – Session
  • Write your own Web server in C#, part 5 – view engine
  • Writing your own Web server in C#, part 6 – user authentication