Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / database / MySQL

Authorization on Self-Hosted SignalR Server with a MySql DB with EntityFramework

4.93/5 (6 votes)
19 Nov 2015CPOL3 min read 24.2K  
How to get a claimbased cookie authentication between custom frontend and self hosted SignalR server

Introduction

Recently, I ran across the problem of authorizing on a self-hosted SignalR console application. When I would ask friends, the first question was: Why not in an ASP.NET environment? Because my console application is more of a daemon than anything else. There isn't much logic behind it but I couldn't get around the login process. I've started to Google quite a bit and I still have not found a public solution available to the initial problem. This is why I am posting this article as there doesn't seem to be much information about it. Not even on the Microsoft SignalR forum.

This article will require some basic knowledge about the environment we are going to use. I will not explain MySQL nor knockoutjs and most certainly not EF. The intention of this article is to get your basic setup going and advance from there!

Background

Setup/Key functions:

  • Entity Framework 6
  • MySQL
  • SignalR Self-hosted (server)
  • Knockoutjs (client)

Using the Code

We will start off by setting up the SignalR server using the simple C# console application template in VS.
Install the following nuget package: 'MySql.Data.Entity'.
This will install the following packages as well:

  • MySql.Data
  • EntityFramework

Then install 'Microsoft.AspNet.SignalR.SelfHost', 'Microsoft.Owin.StaticFiles' and 'Microsoft.Owin.Security.Cookies'.
Quick explanation: We will serve a static authentication file for the login process. A simple form. And save a claims based identity in the cookie.
This will be our basic setup for the serverside.

Next thing we need to do is to set our db connection up with the help of EF. Add a basic user class consisting only of username and password.

C#
public class User
{
    //KEY
    public Int32 Id { get; set; }
    //loginname and displayname
    public String Username { get; set; }
    //password
    public String Password { get; set; }
}

Then set the context up for EF.

C#
[DbConfigurationType(typeof(MySql.Data.Entity.MySqlEFConfiguration))]
public class GameContext : DbContext
{
    public GameContext()
        : base("standard")
    {

    }

    public DbSet<user> Users { get; set; }
}

Prepare a simple user table for the logindata.

SQL
-- phpMyAdmin SQL Dump
-- version 4.1.12
-- http://www.phpmyadmin.net
--
-- Host: 127.0.0.1
-- Erstellungszeit: 19. Nov 2015 um 13:12
-- Server Version: 5.6.16
-- PHP-Version: 5.5.11

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;

--
-- Datenbank: `database`
--

-- --------------------------------------------------------

--
-- Tabellenstruktur für Tabelle `users`
--

CREATE TABLE IF NOT EXISTS `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` text NOT NULL,
  `password` text NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ;

--
-- Daten für Tabelle `users`
--

INSERT INTO `users` (`id`, `username`, `password`) VALUES
(1, 'user', 'pass');

/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

Then add the validation process in a UserManager class.

C#
public class UserManager
{
    private GameContext zGC = new GameContext();

    public User Authenticate(String pUsername, String pPassword)
    {
	//returns null if no user found
        return this.zGC.Users.FirstOrDefault
        (x => x.Username == pUsername && x.Password == pPassword);
    }
}

Then add the connection string to your app.config file.

XML
<add name="standard" providerName="MySql.Data.MySqlClient.EF6" 
connectionString="Server=localhost;Database=database;Uid=username;Pwd=5FttZWuS6rMnCwLX; />

Add this point we are set to add and retrieve user information. So the next task is to set SignalR up.

To fire up the server, add the following:

C#
string lServerURL = "http://+:19221";
WebApp.Start<startup>(lServerURL);
Console.WriteLine("Server launched on => " + lServerURL + "\r\n");
Console.Read();

Now, we already get to the interesting part. The configuration.
It'll basically come down to the following:
 

Image 1

Add the following code:

C#
public class Startup
{
    private const String _WebsiteUrl = "Http://www.website.com";
    private static String _Authpage = File.ReadAllText(Path.Combine
	(Path.Combine(Environment.CurrentDirectory, @"..\.."), @"Auth/Index.html"));

    public void Configuration(IAppBuilder app)
    {
        //So we can actually load the hubs even when we are on a different domain
        app.UseCors(CorsOptions.AllowAll);
        string lContentPath = Path.Combine(Environment.CurrentDirectory, @"..\..");
        var fileOptions = new StaticFileOptions
        {
            FileSystem = new PhysicalFileSystem(lContentPath),
        };

        app.UseStaticFiles(fileOptions);
        if (!String.IsNullOrEmpty(_Authpage))
            _Authpage = File.ReadAllText(Path.Combine(lContentPath, @"Auth/Index.html"));

        CookieAuthenticationOptions lOptions = new CookieAuthenticationOptions()
        {
            AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
            LoginPath = new PathString("/Auth/Login"),
            LogoutPath = new PathString("/Auth/Logout"),
        };

        app.UseCookieAuthentication(lOptions);

        app.Use(async (context, next) =>
        {
            var redirectUri = context.Request.Query["ReturnUrl"];

            //If accesses the login path and is not logged in and is a POST request
            if (context.Request.Path.Value.Contains(lOptions.LoginPath.Value) && 
			context.Authentication.User == null)
            {
                if (context.Request.Method == "POST")
                {
                    //Do we have the needed form data to validate the user
                    var lForm = await context.Request.ReadFormAsync();
                    if (!String.IsNullOrEmpty(lForm["input_login"]) && 
				!String.IsNullOrEmpty(lForm["input_pass"]))
                    {
                        //Validate him
                        var lAuthenticatedUser = new UserManager().Authenticate(lForm["input_login"], 
				lForm["input_pass"]);
                        if (lAuthenticatedUser != null)
                        {
                            //If the user exists, set claims
                            ClaimsIdentity lIdentity = new ClaimsIdentity(lOptions.AuthenticationType);
                            lIdentity.AddClaim(new Claim(ClaimTypes.Name, lAuthenticatedUser.Username));

                            //log user in
                            context.Authentication.SignIn(lIdentity);

                            //send user (authorized) to our game site
                            context.Response.Redirect(_WebsiteUrl);

                            //At this point the user will be able to connect to the hub 
		            //AND access methods with the [Authorize] tag
                        }
                    }
                }
                else
                {
                    //If not, send him back to the login page
                    context.Response.Redirect("/Auth/");
                }
            }
            else if (context.Request.Path.Value.Contains(lOptions.LogoutPath.Value))
            {//If the user is accessing the logout path

                //log him out
                context.Authentication.SignOut(lOptions.AuthenticationType);

                //and send him back to the login screen
                context.Response.Redirect("/Auth");
            }
            else if (context.Request.Path.Value == "/Auth/")
            {
                //Is the user already logged in, send him to the game site
                if (context.Authentication.User != null)
                    context.Response.Redirect(_WebsiteUrl);


                context.Response.ContentType = "text/html";
                //Send him the login form
                await context.Response.WriteAsync(_Authpage);
            }
            else
            {
                //If none of the above applies
                await next();
            }
        });

        app.MapSignalR();
    }
}

Now if you visit: http://localhost:19221/Auth/, you can successfully login and you will see that a cookie will be set. We are successfully authenticated to our SignalR server.

Image 2

Let's get a basic hub going which we can connect to by adding a new class and inheriting from 'hub', we will also give it the [Authorize] tag to make sure it's only accessible if logged in.

C#
public class systemHub : Hub
{
    [Authorize]
    public void getCurrentDate()
    {
        //we will call the updateCurrentDate function in our javascript
        Clients.Caller.updateCurrentDate("[" + 
        DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss") + "]");
    }
	
	// Incase we need to find out if we are logged in
    public Boolean IsAuthenticated(String pCallbackFunc)
    {
        var lLoggedIn = false;
        if (Context.User != null)
        {
            if (Context.User.Identity.IsAuthenticated)
            {
                lLoggedIn = true;
            }
        }
        
        Clients.Caller.updateLoginStatus(lLoggedIn);
    }
}

This is pretty much it for our server side. Now we will go ahead and add a new project for our frontend that will connect to our hub and display us the date. For this purpose, we will be using the "ASP.NET Empty Web Application" template. Start it up and add the url to your Startup file.

C#
private const String _WebsiteUrl = "http://localhost:52729/";

Let's finish this project up with a simple ViewModel a connection to our hub and a button to check whether we are connected or not and another button to get the current server time.

HTML
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <style>
        .hidden{display: none;}
    </style>
</head>
<body>
    <h1 data-bind='text: loginStatus'>Loading....</h1>
    <h2 data-bind='text: serverDate'></h2>
    <button id="statusButton" class="hidden">getStatus</button>
    <button id="timeButton" class="hidden">getDateTime</button>

    <script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
    <script src="http://ajax.aspnetcdn.com/ajax/signalr/jquery.signalr-2.2.0.min.js"></script>
    <script src="http://localhost:19221/signalr/hubs"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js" 
	    type="text/javascript"></script>
    <script>
        function ViewModel() {
            var self = this;
            self.loginStatus = ko.observable($('h1').text());
            self.serverDate = ko.observable("");

            self.initSignalr = function(){
                var baseSignalrServerURL = 'http://localhost:19221/';
                $.connection.hub.url = baseSignalrServerURL + 'signalr';
                var lHub = $.connection.systemHub;

                lHub.client.updateCurrentDate = function(pVal){
                    self.serverDate(pVal);
                }
                lHub.client.updateLoginStatus= function (pVal) {
                    self.loginStatus(pVal);
                }

                //Connect to the hub
                $.connection.hub.start().done(function () {
                    //Once connected, display button
                    $('button').toggleClass('hidden');
                });

                $('#statusButton').click(function () {
                    lHub.server.isAuthenticated();
                });

                $('#timeButton').click(function () {
                    lHub.server.getCurrentDate();
                });
            };
            self.initSignalr();            
        };

        ko.applyBindings(new ViewModel());
    </script>
</body>
</html>

You are now able to browse your frontend and check whether you are logged in or not.

Points of Interest

  • Browse /Auth/Login if you want to log in.
  • Browse /Auth/Logout if you want to log out
  • Set your SignalR app as Startup project, otherwise you won't be able to load the hubs file

Image 3 Image 4

History

  • 19th November, 2015: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)