Early on this year, I published an article describing how to integrate ASP.NET Core SignalR and Vue.js. Through said article, a minimalistic version of Stack Overflow was built in order to showcase how all these technologies can be integrated into a working application.
Setting up an application where all these technologies collaborate to create a simple but working version of one of the most popular (if not the most popular) website for developers, made for a long albeit interesting article. However, in order to maintain the focus of the article and keep its length under control, there was a big topic not covered in that article: Authentication!
In this article, we will revisit the same application built in the first article with the sole aim of discussing authentication.
Editorial Note: Make sure to read the first article Using ASP.NET Core SignalR with Vue.js (to create a mini Stack Overflow rip-off) in order to understand this one.
We will start with cookie based authentication, one of the most widely used options in web applications that many of you might be familiar with. Then we will investigate how an application can support different authentication schemes (or mechanisms). That will allow us to introduce a second authentication scheme based on JWT Bearer tokens, which is sometimes favored by SPA and mobile applications.
Once users can login into our site (Figure 1), we will see how SignalR seamlessly integrates with ASP.NET Core authentication, adding a simple live chat to the application (Figure 2). Along the way, we will also see how Vuex can be used to elegantly solve the problem of shared data in a Vue application!
There is no question about it, security is a complex topic. I hope you will find this article both useful and interesting, giving you enough information to understand the various authentication choices and tools available for your Vue and SignalR applications.
The companion source code for the article can be found on GitHub. If you want to follow along with the article, use the branch authentication-start which does not contain any of the authentication code changes.
Figure 1, users will now be able to login
Figure 2, a simple live chat will be created
Cookie based authentication for ASP.NET Core and SignalR app
The application we will use throughout this article provides users with a basic site similar to the popular Stack Overflow site. It lets them create questions, provide answers , and even let’s them vote the Q&A.
As we saw in the previous article, SignalR was used to provide real time updates of votes and answers.
However, authentication was nowhere to be seen! In this first section of the article, we will update the application so users can login/logout and the site is effectively read only for anonymous users, including the SignalR hubs.
We will start following the most common authentication scheme: cookie based authentication. At a very high-level, it works like this:
1. The browser sends user entered credentials (like username and password) for a server to validate.
2. If the server determines the credentials are valid, it generates an encrypted cookie used to identify the user and includes a Set-Cookie header in the response sent back to the browser.
3. The browser receives the response and reads the Set-Cookie header, saving the cookie to the cookie jar.
4. Upon any further requests, the browser automatically includes the cookie within the requests.
5. The server inspects the received headers on every request, expecting to find the authentication cookie it sent upon authentication. In order to authorize the request, it can decrypt and verify the cookie contents.
Of course, things are a little more complicated. There are multiple ways a server can login a user (not just username and password, for example OAuth with 3rd party services like Google or Twitter), and cookies themselves need to be configured to be secure. (They shouldn’t be accessible to JavaScript, ideally sent over HTTPS only and restricted to specific domain/sites).
ASP.NET Core Identity takes care of it all, providing a complete solution and a very convenient way of adding authentication to ASP.NET Core web applications.
However, there is a problem with so much convenience, and that is, its controllers and views are geared towards traditionally server-side rendered applications! That is, Razor pages/views will render elements like login forms, these in turn will send full page POST requests to the controllers, which finally respond with a redirect back to the home page.
This is nothing but the well-known Post/Redirect/Get pattern.
This might not work so well in the context of SPAs applications like the one used in this article (unless you can live with full page posts and redirects in your authentication pages). Ideally, the server will just provide an authentication API, leaving to the client side of the SPA (the Vue application in our case), the UX workflow.
Cookie based authentication API
We will then begin by introducing a new API into our server side ASP.NET Core application in order to provide cookie-based authentication.
In order to maintain pace and focus, during this article, we will leave aside 3rd party OAuth providers and consider local accounts only (many of the problems and techniques you will face are similar, so you will be better equipped once you understand local accounts! Who knows, might be the subject of a future article?)
There are two ways we can build such an API.
We could use the scaffolding provided by ASP.NET Core Identity or we could manually write the controller using the Cookie authentication services.
In this article, I will manually write the controllers due to the following reasons:
– The controller code generated by the scaffolder for login/logout actions assumes the application will use full posts followed by redirects, instead of an API called from JavaScript.
– We need to write the client elements ourselves as part of our Vue application.
– I will not include code to manage accounts, only to login/logout.
However, there will be nothing wrong if you decide to use the provided scaffolding. Simply discard the generated views and manually modify the generated controller code.
Enough about setting up the context, let’s start writing some code! The first thing we are going to do is to enable the necessary services and middleware in our Startup class. First let’s define a new constant for the Cookie authentication scheme:
public const string CookieAuthScheme = "CookieAuthScheme";
Next, add the following code to the ConfiguresServices method. It will add the authentication services using Cookies based authentication as the default scheme:
// Add Authentication services, using
services.AddAuthentication(CookieAuthScheme)
// Now add and configure cookie authentication
.AddCookie(CookieAuthScheme, options =>
{
// Set the cookie name (optional)
options.Cookie.Name = "soSignalR.AuthCookie";
// Set the samesite cookie parameter as none,
// otherwise it won’t work with clients on uses a different domain wont work!
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;
// Simply return 401 responses when authentication fails
// as opposed to the default of redirecting to the login page
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = redirectContext =>
{
redirectContext.HttpContext.Response.StatusCode = 401;
return Task.CompletedTask;
}
};
})
Hopefully the code is self-explanatory.
Let me direct your attention to the SameSite cookie setting. Since ASP.NET Core 2.2, its default is lax, which means the browser will only include the cookie in requests to different sites for GET requests.
If you remember from the earlier article, our client and server side applications are deployed independently. Even during development, the client application runs in localhost:8080 while the server runs in localhost:5100. This means we need to change the default SameSite setting or we won’t be able to authenticate our app (as long as they are deployed as different sites). Of course, if you are deploying both client and server side from the same site, you should leave this with its default lax value!
Now that all the required services are added and configured, update the Configure method to add the authentication middleware right after the CORS middleware:
app.UseAuthentication();
This is the middleware that extracts user information from the request (using the configured scheme), enabling the application to perform authentication challenges, for example when adding the [Authorize] attribute.
Before we continue, notice how we are not adding the Identity services that provide the functionality to create, retrieve and validate user accounts. There is good documentation on how to add Identity services, while at the same time it will add significant noise to the article (relies on using a database and Entity Framework, none of which are used by our sample application).
As you will see, when we look at the AccountController implementation, we will simulate the Identity functionality by manually validating user credentials and manually creating the ClaimsPrincipal instances.
Let’s finish the server side changes by adding a new AccountController that provides the new /account API with login and logout endpoints:
public class LoginCredentials
{
public string Email { get; set;}
public string Password { get; set;}
}
[Route("[controller]")]
public class AccountController : Controller
{
[HttpPost("login")]
public async Task Login([FromBody]LoginCredentials creds)
{
// We will typically move the validation of credentials
// and return of matched principal into its own AuthenticationService
// Leaving it here for convenience of the sample project/article
if (!ValidateLogin(creds))
{
return Json(new
{
error = "Login failed"
});
}
var principal = GetPrincipal(creds, Startup.CookieAuthScheme);
await HttpContext.SignInAsync(Startup.CookieAuthScheme, principal);
return Json(new
{
name = principal.Identity.Name,
email = principal.FindFirstValue(ClaimTypes.Email),
role = principal.FindFirstValue(ClaimTypes.Role)
});
}
[HttpPost("logout")]
[Authorize]
public async Task Logout()
{
await HttpContext.SignOutAsync();
return StatusCode(200);
}
// On a real project, you would use a SignInManager to verify the identity
// using:
// _signInManager.PasswordSignInAsync(user, password, lockoutOnFailure: false);
// With JWT you would rather avoid that to prevent cookies being set and use:
// _signInManager.UserManager.FindByEmailAsync(email);
// _signInManager.CheckPasswordSignInAsync(user, password, lockoutOnFailure: false);
private bool ValidateLogin(LoginCredentials creds)
{
// For our sample app, all logins are successful!
return true;
}
// On a real project, you would use the SignInManager
// to locate the user by its email and build its ClaimsPrincipal:
// var user = await _signInManager.UserManager.FindByEmailAsync(email);
// var principal = await _signInManager.CreateUserPrincipalAsync(user)
private ClaimsPrincipal GetPrincipal(LoginCredentials creds, string authScheme)
{
// Here we are just creating a Principal for any user,
// using its email and a hardcoded “User” role
var claims = new List
{
new Claim(ClaimTypes.Name, creds.Email),
new Claim(ClaimTypes.Email, creds.Email),
new Claim(ClaimTypes.Role, “User”),
};
return new ClaimsPrincipal(new ClaimsIdentity(claims, authScheme));
}
}
This is very similar to the code you might have seen so far in scaffolded account controllers. The most important bits are two lines that actually perform the login and logout functionality:
– In the login method, await HttpContext.SignInAsync(Startup.CookieAuthScheme, principal); uses the Cookie scheme we configured earlier in the Startup class in order to generate a Cookie and include it a Set-Cookie header in the HTTP response.
– In the logout method, await HttpContext.SignOutAsync(); uses the Cookie scheme and includes another Set-Cookie header in the HTTP response that instructs the browser to remove the cookie.
There are also a few differences worth discussing when compared against the classic code provided vs the one scaffolded by ASP.NET Core Identity:
– The controller actions do not return a ViewResult nor a RedirectResult. Instead they return JsonResult and StatusCodeResult! This is vital for the Vue application to call this API using JavaScript.
– The login controller expects credentials to be received as part of the body, so the client can send them as a JSON.
– As mentioned earlier, we are not using the Identity services like the SignInManager class to validate user credentials and create ClaimsPrincipal instances. Instead we are replacing that with stub functionality that will let anyone to authenticate! Replace these methods with real implementations in your application.
At this point you should be able to test your API using any tool like Postman or cURL to send a JSON with some username and password credentials. You should see in the response, the Set-Cookie header:
Figure 3, testing the login endpoint using cURL
That’s it, we have a simple but functional API that allows the Vue application to use JavaScript in order to login and logout from the application.
Let’s turn our attention to the client side.
Adding authentication functionality to the Vue client
Now that our server provides a simple authentication mechanism, we need to update the Vue application with the necessary elements so users can login by entering their credentials and logout if already authenticated.
We will update the navbar to show a Login button on the top right. Upon being clicked, a modal will be displayed for users to enter their credentials:
Figure 4, the first iteration of the login modal, opened from the Login button in the navbar
Once the user enters the credentials, we will send an AJAX request to the login endpoint, and will update the navbar so it now displays the user name and a Logout button:
Figure 5, once logged in, the navbar will display the username and Logout button
This brings some interesting design questions, particularly around where should the data identifying the currently logged in user be stored.
– Should that be in the root App.vue component, passed as props to any child component like the navbar?
– What happens when authenticating in a modal component? Should events be propagated up across all the component tree until it reaches App.vue where the data is finally updated?
– How can any component know if the user is authenticated or not, for example in order to disable some buttons?
Luckily for us, Vuex is the perfect answer for shared data like the current user context, data that belongs to none and all components! Apart from being the perfect answer to this problem you will see how using it is quite straightforward. (If you want to learn more, check out one of my previous article taking a closer look at Vuex)
Now that we know what we will build and how, let’s begin.
The first thing we will do is to extract the main navbar from the App.vue component into its own component. Create a new main-navbar.vue file inside the components folder, and copy the navbar from App.vue into the section of the component.
Then import the new main-navbar component inside the App.vue script section:
import MainNavbar from './components/main-navbar'
export default {
name: 'App',
components: {
MainNavbar
},
…
}
And finally replace the navbar in the App.vue script section with the component we just included: .
Let’s now create the login modal component, where we will make use of bootstrap-vue’s modal component (as in the existing modal for adding questions and answers):
<template>
<b-modal id="loginModal" ref="loginModal" hide-footer title="Login" @hidden="onHidden">
<b-form @submit.prevent="onSubmit" @reset.prevent="onCancel">
<b-alert show variant="warning">In this test app, any credentials are valid!</b-alert>
<b-form-group label="Email:" label-for="emailInput">
<b-form-input id="emailInput"
type="email"
v-model="form.email"
required
placeholder="Enter your email address">
</b-form-input>
</b-form-group>
<b-form-group label="Password:" label-for="passwordInput">
<b-form-input id="passwordInput"
type="password"
v-model="form.password"
required
placeholder="Enter your password">
</b-form-input>
</b-form-group>
<button class="btn btn-primary float-right ml-2" type="submit">Login</button>
<button class="btn btn-secondary float-right" type="reset">Cancel</button>
</b-form>
</b-modal>
</template>
<script>
export default {
data () {
return {
form: {
email: '',
password: ''
}
}
},
methods: {
onSubmit (evt) {
// to be completed
},
onCancel (evt) {
this.$refs.loginModal.hide()
},
onHidden () {
Object.assign(this.form, {
email: '',
password: ''
})
}
}
}
</script>
Nothing too exciting here. Just some regular Vue code providing a modal, and an empty onSubmit method which we will come back to later!
Before we can display the modal, it needs to be part of the Vue application. Follow the same steps we took to include the main-navbar component inside App.vue. With this, the modal is ready to be displayed, all we need is a button!
Update the main-navbar component and replace the form providing a sample search box with a form that provides a Login button. This button uses bootstrap-vue’s v-b-modal directive to show the login modal we just created and wired inside App.vue:
Login
If you run the application, you should see the modal appearing after clicking on the Login button. However, we left the onSubmit method empty, so it will do nothing yet!
Using Vuex to store the shared authentication context data
To implement the login functionality and keep the user context data, we will use Vuex. The very first thing to do is to install the library. Run the following command from the root folder of the client application:
npm install --save vuex
Once installed, create a new folder named store inside the client/src folder. Create two new files, named index.js and context.js. The first one, index.js, will be used to wire Vuex into the Vue application and to compose together all the different Vuex modules:
import Vue from 'vue'
import Vuex from 'vuex'
import context from './context'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
context
}
})
The second file, context.js, will provide a module for everything related with the user context. Let’s start with an empty module:
export default {
namespaced: true,
state: {
},
getters: {
},
mutations: {
},
actions: {
}
}
Don’t worry, we will fill it up as we build the functionality. Let’s start by providing the code necessary to perform the login action. This code will send a request to the login endpoint of our server-side API, and will save the returned user profile into the module state:
export default {
namespaced: true,
state: {
profile: {}
},
getters: {
isAuthenticated: state => state.profile.name && state.profile.email
},
mutations: {
setProfile (state, profile) {
state.profile = profile
},
},
actions: {
login ({ commit }, credentials) {
return axios.post('account/login', credentials).then(res => {
commit('setProfile', res.data)
})
},
logout ({ commit }) {
return xios.post('account/logout').then(() => {
commit('setProfile', {})
})
}
}
}
Our context module now provides:
– a login action that the login modal can use. This action will send a request to the server API and will update the module state with the returned user profile
– a logout action that the navbar can use. Similar to the login action, this will send a request to the server API and will clear out the current profile from the module’s state
– a profile property in its state, which any component can map. For example, the navbar can include a Welcome, username message when logged in.
– an isAuthenticated getter that any component can map. This returns a Boolean indicating whether the user is currently logged in or not, which will be widely used. For example, the navbar can use it to render either a login or a logout button; while buttons that require authentication can be disabled based on its value.
Let’s finish with the login process. Update the login modal to map the login action of the module:
import { mapActions } from 'vuex'
export default {
...
methods: {
...mapActions('context', [
'login'
]),
onSubmit (evt) {
this.login({ authMethod: this.authMode, credentials: this.form }).then(() => {
this.$refs.loginModal.hide()
})
},
...
}
}
Here, we are just mapping the action from the context store, calling it when the form is submitted, and closing the modal once the action succeeded.
Next, let’s update the navbar so it displays either of the following:
– a login button
– the username and a logout button
..based on the data currently stored in the context store. It is as simple as mapping the logout action, the profile property of the state (so we can render the profile.name property) and the isAuthenticated getter (so we can decide between the two options in the template).
Replace the login form of the template section with:
Welcome back, {{ profile.name }}
Logout
Login
Of course, the script section needs to be updated so it maps these elements from the context module (otherwise they wouldn’t be available in the template):
import { mapGetters, mapState, mapActions } from 'vuex'
export default {
computed: {
...mapState('context', [
'profile'
]),
...mapGetters('context', [
'isAuthenticated'
])
},
methods: {
...mapActions('context', [
'logout'
])
}
}
That’s it, now you should be able to login and logout from the application. There is a little problem however. As soon as you reload the page, you will appear as logged out, even if your browser still has the auth cookie!
This is because our components rely on the state kept in the Vuex store, which is gone as soon as you reload the page, since it is kept in memory. We will need to restore this context when our Vue application starts!
In order to solve this problem, we will include a new endpoint in our server-side API to load the details of the currently logged in user (Note how the properties will be empty in case the user isn’t currently authenticated, so the isAuthenticated getter of the client application detects it):
[HttpGet("context")]
public JsonResult Context()
{
return Json(new
{
name = this.User?.Identity?.Name,
email = this.User?.FindFirstValue(ClaimTypes.Email),
role = this.User?.FindFirstValue(ClaimTypes.Role),
});
}
We will then provide a new Vuex action to call this endpoint and update the store profile state with its response:
restoreContext ({ commit}) {
return axios.get('account/context').then(res => {
commit('setProfile', res.data)
})
},
Finally, we will call this endpoint from the App.vue component mounted state:
import { mapActions } from 'vuex'
…
export default {
…
created () {
this.restoreContext()
},
methods: {
...mapActions('context', [
'restoreContext'
])
}
}
After these changes, you should be able to login/logout and stay logged in when reloading the page.
That was quite a journey, but we now have the basic functionality wired end to end and we can start with the more interesting parts!
Securing the application
Securing the REST API
Since our users can now login and logout, we can start restricting parts of our application to authenticated users. Let’s begin with the controller actions to create new questions, up/down vote them and create new answers.
This is as simple as adding the [Authorize] attribute on all these endpoints. The attribute will enforce users to be authenticated, so as long as users are logged in, the cookie will be sent and the attribute will grant access to the controller endpoint.
Sadly, if you try to add a question, you will notice the site no longer works after we added the [Authorize] attribute. Even when you are authenticated, the server returns a 401 response!
The problem lies again in the fact that client and server are running as different applications, one at localhost:8080 and the other at localhost:5100. When this happens, browsers will not include cookies along with AJAX requests, unless specifically instructed to do so.
So all you have to do, is to update main.js and setup the axios defaults so credentials (like cookies) are included along requests:
axios.defaults.baseURL = 'http://localhost:5100'
axios.defaults.withCredentials = true
Note, this topic is closely related with the topic of CORS! The CORS server side middleware was configured during the first article to allow communication between the client and server applications.
Of course, if your application will end up deployed with the client and server on the same domain, you would not need to worry about these issues. If this is your case, you can add a vue.config.js file that points the Vue development server towards your ASP.NET Core server. This means, from your browser point of view, everything will be running in localhost:8080 and you won’t have to face these cross-site issues.
Now that we have solved this small hiccup, our application is working again!
Authenticated users can create questions, up/down vote them and create answers. However, anonymous users can still attempt to perform these actions, just to get a 401 response in return.
We can very easily provide them with a better UX where buttons that trigger actions unavailable to anonymous users, are disabled or invisible.
Remember the isAuthenticated getter we added to the context Vuex store? This is another use case where Vuex shines.
For example, update the home.vue component so the add question button is disabled based on the isAuthenticated getter. All you have to do is to map the getter and use it to set the disabled attribute of the button:
// In the component template
<button v-b-modal.addQuestionModal :disabled="!isAuthenticated" class="btn btn-primary mt-2 float-right">
<i class="fas fa-plus"/> Ask a question
</button>
// In the component script
computed: {
...mapGetters('context', [
'isAuthenticated'
])
…
},
Rinse and repeat! You can follow the same approach to disable/hide any links that trigger actions available only for authenticated users. (Feel free to check the final code on github)
Securing the SignalR hub
Securing the SignalR hub is as simple as adding the [Authorize] attribute to either the Hub class or individual Hub methods.
Let’s add the attribute to our QuestionHub class. That was easy, right?
Well, hold on!
If you open an incognito window or logout and reload the page, you will notice an endless series of calls to http://localhost:5100/question-hub/negotiate that end in 401.
Figure 6, having trouble connecting to the SignalR hub
This is because our Vue application will try to connect to the SignalR hub as soon as the application starts, regardless of whether the user is authenticated or not. What’s worse, we included some code to automatically reconnect, which ends in this endless loop.
We need to rethink this behaviour.
Since our QuestionHub now requires users to be authenticated we should then:
– On application startup, only start a connection with the hub if we are logged in
– Start a connection after a successful login action
– Stop the connection after a logout action
Luckily for us, the question-hub.js Vue plugin we created and the Vuex context module can easily play together in order to achieve this behavior in a way that’s transparent for the rest of the application!
Let’s start with the question-hub plugin. Rather than automatically trying to establish a connection on application startup, we will provide with methods to start and stop the connection:
export default {
install (Vue) {
// use a new Vue instance as the interface for Vue components
// to receive/send SignalR events. This way every component
// can listen to events or send new events using this.$questionHub
const questionHub = new Vue()
Vue.prototype.$questionHub = questionHub
// Provide methods to connect/disconnect from the SignalR hub
let connection = null
let startedPromise = null
let manuallyClosed = false
Vue.prototype.startSignalR = (jwtToken) => {
}
Vue.prototype.stopSignalR = () => {
}
// Provide methods for components to send messages back to server
// Make sure no invocation happens until the connection is established
questionHub.questionOpened = (questionId) => {
if (!startedPromise) return
return startedPromise
.then(() => connection.invoke('JoinQuestionGroup', questionId))
.catch(console.error)
}
questionHub.questionClosed = (questionId) => {
if (!startedPromise) return
return startedPromise
.then(() => connection.invoke('LeaveQuestionGroup', questionId))
.catch(console.error)
}
}
}
As you can see, the questionHub can be created straight away, meaning that components can add listeners to SignalR events regardless of whether we are connected or not. (If we are not connected, then they will never receive an event through the questionHub).
We are also checking if the connection process has been started before trying to send an event through the SignalR connection. Since the connection might be instantiated but not fully opened, this is a little more complicated than checking if it is not null. We will see more once we implement the start/stop methods.
Implementing the start method is mostly moving the initialization code, inside this method:
Vue.prototype.startSignalR = (jwtToken) => {
connection = new HubConnectionBuilder()
.withUrl(`${Vue.prototype.$http.defaults.baseURL}/question-hub`)
.configureLogging(LogLevel.Information)
.build()
// Forward hub events through the event, so we can listen for them in the Vue components
connection.on('QuestionAdded', (question) => {
questionHub.$emit('question-added', question)
})
connection.on('QuestionScoreChange', (questionId, score) => {
questionHub.$emit('score-changed', { questionId, score })
})
connection.on('AnswerCountChange', (questionId, answerCount) => {
questionHub.$emit('answer-count-changed', { questionId, answerCount })
})
connection.on('AnswerAdded', answer => {
questionHub.$emit('answer-added', answer)
})
// You need to call connection.start() to establish the connection but the client wont handle reconnecting for you!
// Docs recommend listening onclose and handling it there.
// This is the simplest of the strategies
function start () {
startedPromise = connection.start()
.catch(err => {
console.error('Failed to connect with hub', err)
return new Promise((resolve, reject) => setTimeout(() => start().then(resolve).catch(reject), 5000))
})
return startedPromise
}
connection.onclose(() => {
if (!manuallyClosed) start()
})
// Start everything
manuallyClosed = false
start()
}
This is mostly the same code as before, with the addition of the manuallyClosed flag. Since we are adding a stop method that we will invoke after user’s logout, we need to prevent the reconnecting code from keep trying, something we achieve by updating this flag as true.
Next, implement the stop method, which simply calls the connection stop method and clears our flags:
Vue.prototype.stopSignalR = () => {
if (!startedPromise) return
manuallyClosed = true
return startedPromise
.then(() => connection.stop())
.then(() => { startedPromise = null })
}
All that’s needed is for our context module to automatically call the startSignalR and stopSignalR as a result of the login, logout and restoreContext actions! Notice how we added the methods to the Vue.prototype earlier, so we can call them from the store:
import Vue from 'vue'
…
actions: {
restoreContext ({ commit, getters, state }) {
return axios.get('account/context').then(res => {
commit('setProfile', res.data)
if (getters.isAuthenticated) return Vue.prototype.startSignalR()
})
},
login ({ commit }, credentials) {
return axios.post('account/login', credentials).then(res => {
commit('setProfile', res.data)
}).then(() =>
Vue.prototype.startSignalR()
)
},
logout ({ commit, state }) {
return axios.post('account/logout').then(() => {
commit('setProfile', {})
return Vue.prototype.stopSignalR()
})
}
}
That’s it, the endless loop of 401 requests trying to connect to the hub when not authenticated, should be gone now.
You will also notice the browser starting/stopping the connection as soon as you login/logout from the app. Of course, the functionality provided by the hub should work as long as you are logged in, for example open two browser windows, login in both and try to add new answers and votes.
Take a moment to notice how no other component of our Vue application except for these two files, had to be modified!
Adding a Live Chat
After all this hard work, let’s have a little fun by adding a simple chat to our application! With all the building blocks we have so far, this will require little work.
On the server side, all we need to do is to:
– add a new method to our IQuestionHub interface that defines the event received by clients when a message is sent to the chat
– add a new method to the QuestionHub class that clients can send an event to, when they want to send an event to the chat
These changes look like the following:
public interface IQuestionHub
{
...
Task LiveChatMessageReceived(string username, string message);
}
[Authorize]
public class QuestionHub: Hub
{
...
public async Task SendLiveChatMessage(string message)
{
await Clients.All.LiveChatMessageReceived(Context.UserIdentifier, message);
}
}
Which means we are implementing a general chat where all messages are sent to everyone.
There is one little extra detail to take care of.
Notice the usage of Context.UserIdentifier in the method implementation. We basically want to include the user name along the event payload, so we can display the name of the user who sent each message.
We need to tell SignalR how to extract this user identifier from the ClaimsPrincipal object that results from a successful authentication. Implement the IUserIdProvider interface, for example we will use the principal’s name, since we were setting it from the email address:
public class NameUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
{
return connection.User?.Identity?.Name;
}
}
Then include this as part of the ConfigureServices method of the Startup class:
services.AddSingleton();
That completes the server-side part.
On the frontend, let’s start by updating the question-hub.js with the new listener for the LiveChatMessageReceived event, and the new method to call the SendLiveChatMessage event:
connection.on('LiveChatMessageReceived', (username, text) => {
questionHub.$emit('chat-message-received', { username, text })
})
...
questionHub.sendMessage = (message) => {
if (!startedPromise) return
return startedPromise
.then(() => connection.invoke('SendLiveChatMessage', message))
.catch(console.error)
}
Next let’s create a new modal where the users can see the messages received and send new messages. Add a new live-chat-modal.vue file inside the components folder with the following contents:
<template>
<b-modal id="liveChatModal" ref="liveChatModal" hide-footer title="Live Chat" size="lg" @hidden="onHidden">
<div class="bg-light messages-container">
<ul v-if="messages.length" class="list-unstyled container">
<li v-for="(message, index) in messages" :key="index" class="row my-2">
<span class="col-3">
{{ message.username === profile.name ? 'You' : message.username }}
</span>
<vue-markdown
:class="{'col-9': true, 'text-muted': message.username === profile.name}"
:source="message.text" />
</li>
</ul>
<p v-else class="text-muted text-center">
Welcome to the chat...<br />
Say hi!
</p>
</div>
<b-form class="border-top mt-2 pt-2" @submit.prevent="onSendMessage">
<b-form-group label="Your message:" label-for="messageInput">
<b-form-textarea
id="messageInput"
v-model="form.message"
placeholder="What do you have to say?"
:rows="2"
:max-rows="10">
</b-form-textarea>
</b-form-group>
<button class="btn btn-primary float-right ml-2" type="submit">Send</button>
</b-form>
</b-modal>
</template>
<script>
import { mapState } from 'vuex'
import VueMarkdown from 'vue-markdown'
export default {
components: {
VueMarkdown
},
data () {
return {
messages: [],
form: {
message: ''
}
}
},
computed: {
...mapState('context', [
'profile'
])
},
created () {
// Listen to answer changes from SignalR event
this.$questionHub.$on('chat-message-received', this.onMessageReceived)
},
beforeDestroy () {
// Make sure to cleanup SignalR event handlers when removing the component
this.$questionHub.$off('chat-message-received', this.onMessageReceived)
},
methods: {
onMessageReceived ({ username, text }) {
this.messages = [...this.messages, { username, text }]
},
onSendMessage (evt) {
this.$questionHub.sendMessage(this.form.message)
this.form.message = ''
},
onHidden () {
Object.assign(this.form, {
message: ''
})
}
}
}
</script>
<style scoped>
.messages-container{
max-height: 450px;
overflow-y: auto;
}
</style>
While it might look scary, it is mostly presentation! Logic-wise, there is not much going on here.
The component starts with an empty array of received messages. It then listens to chat-message-received events, adding them to the array of received messages. Whenever the user clicks on the send button, it then emits the sendMessage event.
It’s important to note that the component will be receiving messages and updating its array regardless of whether the modal is actually visible or not! Let’s update App.vue again to include this new modal as part of its template, and finally update the home.vue component with a button to show the modal:
Live chat
That’s all that is required to add a functional chat to your application! Feel free to expand on it and add more functionality like private chats or a list of connected members!
JWT Bearer authentication
We now have a fully functional application where users can login and access secured APIs and SignalR hubs, implemented using Cookie based authentication.
While this might be ideal in many scenarios, some people might want/need to use JSON Web Tokens, particularly those in the context of SPAs and mobile applications. If this sounds new to you, don’t worry, there are plenty of articles out there comparing both options like this one or this one, apart from the suspect questions in stack overflow.
I will leave aside (the article is already quite long as it is!) design considerations like when to use JWT instead of Cookies, where to securely store them or how to refresh the tokens, leaving these questions for you to answer based on your needs and context. However, I want to provide an example that uses JWT so you can see what this means in practical terms for SignalR and Vue.
Allowing the server to choose between multiple authentication schemas
Our server currently supports a single authentication scheme, the Cookie based one. However, ASP.NET Core supports multiple authentication schemas as long as we tell it how to choose between them:
public const string JWTAuthScheme = "JWTAuthScheme";
…
services.AddAuthentication(CookieAuthScheme)
// Now configure specific Cookie and JWT auth options
.AddCookie(CookieAuthScheme, options =>
{
…
// In order to decide the between both schemas
// inspect whether there is a JWT token either in the header or query string
options.ForwardDefaultSelector = ctx =>
{
if (ctx.Request.Query.ContainsKey("access_token")) return JWTAuthScheme;
if (ctx.Request.Headers.ContainsKey("Authorization")) return JWTAuthScheme;
return CookieAuthScheme;
};
})
.AddJwtBearer(JWTAuthScheme, options =>
{
// to be filled
});
We have basically added a second authentication scheme, the one tagged with the JWTAuthScheme constant. We have then added the ForwardDefaultSelector to the default scheme (the CookieAuthScheme) so the framework can choose the right scheme for each request. The logic we are following is based on whether the request contains either of:
– The access_token query string parameter. This is where SignalR will include the token when establishing connections
– The Authorization header. This is where our client application will include the token as part of AJAX requests.
If any of those are found in the incoming request, then we select the JWTAuthScheme scheme. Otherwise we choose the default CookieAuthScheme scheme.
Now we need to configure the JWT scheme:
– Define the key that will be used to sign the tokens
– Define how the token will be validated, for example based on its lifetime
Update the Startup class with:
// NOTE: you want this to be part of the configuration and a real secret!
public static readonly SymmetricSecurityKey SecurityKey =
new SymmetricSecurityKey(
Encoding.Default.GetBytes("this would be a real secret"));
...
.AddJwtBearer(JWTAuthScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
LifetimeValidator = (before, expires, token, param) =>
{
return expires > DateTime.UtcNow;
},
ValidateAudience = false,
ValidateIssuer = false,
ValidateActor = false,
ValidateLifetime = true,
IssuerSigningKey = SecurityKey,
};
});
Notice how we are defining the key using a publicly accessible constant. We will need to access the key from the AccountController once we implement the actual code that logins and generates a token. For the purposes of this app, a hardcoded secret is fine, but in a real application, make sure this is a real secret part of your configuration!
JWT Bearer authentication API
With the changes made in the earlier section, our application will be able to authenticate and authorize users as long as they include a valid token as part of their request.
However, how will the client application get hold of a token?
We need to provide with a new endpoint in the AccountController that verifies the supplied credentials and generates a token instead of a cookie. This is relatively straightforward to implement using the JwtSecurityToken class and the same credentials configured for the JWTAuthScheme:
public class AccountController: Controller
{
// Same key configured in startup to validate the JWT tokens
private static readonly SigningCredentials SigningCreds = new SigningCredentials(Startup.SecurityKey, SecurityAlgorithms.HmacSha256);
private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
...
[HttpPost("token")]
public async Task Token([FromBody]LoginCredentials creds)
{
// We will typically move the validation of credentials
// and return of matched principal into its own AuthenticationService
// Leaving it here for convenience of the sample project/article
if (!ValidateLogin(creds))
{
return Json(new
{
error = "Login failed"
});
}
var principal = GetPrincipal(creds, Startup.JWTAuthScheme);
var token = new JwtSecurityToken(
"soSignalR",
"soSignalR",
principal.Claims,
expires: DateTime.UtcNow.AddDays(30),
signingCredentials: SigningCreds);
return Json(new
{
token = _tokenHandler.WriteToken(token),
name = principal.Identity.Name,
email = principal.FindFirstValue(ClaimTypes.Email),
role = principal.FindFirstValue(ClaimTypes.Role)
});
}
}
This should look very similar to the existing login endpoint, with the difference of generating a token that is manually included in the JSON response as opposed to generating a Cookie sent in a Set-Cookie response header.
Notice how no new logout endpoint or even changes to the existing logout endpoint are needed. That is because for a client to logout when using tokens, they just need to forget that token.
Using JWT with the Vue application
Now that our server can use either a Cookie based authentication scheme or a JWT based one, let’s update our Vue application so users can choose in which way they want to login.
Figure 7, login modal letting you choose between cookies and JWT authentication
Of course, you will never ask the user to make such a decision in a real application, but this will come very handy for the purposes of this application, which is to demonstrate how these features work!
Start by updating the login-modal.vue component, so it includes the radio buttons to select the authentication scheme and passes the selected one down to the context store’s login action:
// On the template
<b-form-group label="Authentication mode">
<b-form-radio-group
id="authMode"
v-model="authMode"
:options="authOptions"/>
</b-form-group>
// On the script
export default {
data () {
return {
...
authMode: 'cookie',
authOptions: [
{ text: 'Cookie', value: 'cookie' },
{ text: 'JWT Bearer', value: 'jwt' }
]
}
},
methods: {
...
onSubmit (evt) {
this.login({ authMethod: this.authMode, credentials: this.form }).then(() => {
this.$refs.loginModal.hide()
})
},
...
}
}
Now the interesting part begins.
The login action of the context store needs to send a request to either the /account/login or the /account/token endpoints based on the authMethod property. It also needs to store the received token in case of the JWT scheme, since we will need to include it as part of the Authorization header on future AJAX requests.
state: {
profile: {},
jwtToken: null
},
mutations: {
...
setJwtToken (state, jwtToken) {
state.jwtToken = jwtToken
}
},
actions: {
...
// Login methods. Either use cookie-based auth or jwt-based auth
login ({ state, dispatch }, { authMethod, credentials }) {
const loginAction = authMethod === 'jwt'
? dispatch('loginToken', credentials)
: dispatch('loginCookies', credentials)
return loginAction.then(() => Vue.prototype.startSignalR())
},
loginCookies ({ commit }, credentials) {
return axios.post('account/login', credentials).then(res => {
commit('setProfile', res.data)
})
},
loginToken ({ commit }, credentials) {
return axios.post('account/token', credentials).then(res => {
const profile = res.data
const jwtToken = res.data.token
delete profile.token
commit('setProfile', profile)
commit('setJwtToken', jwtToken)
})
},
...
}
With these changes, you should now be able to successfully login using the JWT scheme. If you inspect the HTTP requests in your browser developer tools, you should see the token included as part of the response:
Figure 8, response from a successful login using the JWT scheme
Unfortunately, this isn’t enough. If you then try to upvote a question, you will notice a 401 response from the server. That’s is because even though we received a JWT token and we stored it inside our context store, we are not sending it back along AJAX requests.
In order to do so, we will use an axios interceptor. This will be invoked by axios on every request, and it will inspect the context store for a JWT token. In case there is a token, it will automatically add the Authorization header to the request. Update main.js with this interceptor:
axios.interceptors.request.use(request => {
if (store.state.context.jwtToken) request.headers['Authorization'] =
'Bearer ' + store.state.context.jwtToken
return request
})
Notice the format of the header is the constant Bearer followed by the token, separated with a space. That is exactly what the JwtAuthScheme expects on the server! After these changes, you should now be able to interact with the site without receiving 401 responses (except for the SignalR hub, which we haven’t updated yet).
Let’s now make a quick change to the logout endpoint, so we don’t send a request in case of using the JWT scheme, as well as deleting the token from the store:
logout ({ commit, state }) {
const logoutAction = state.jwtToken
? Promise.resolve()
: axios.post('account/logout')
return logoutAction.then(() => {
commit('setProfile', {})
commit('setJwtToken', null)
return Vue.prototype.stopSignalR()
})
}
If everything went right, your users should now be able to login and logout when using the JWT scheme.
However, they will notice something odd.
As soon as they reload the page, they are logged out! The explanation is simple, the token is stored in the vuex store, and that information is gone as soon as you reload the page. We will need to store the token somewhere that survives a simple page refresh!
NOTE: For our purposes, we will simply use local storage. However, you should know that this simple approach has security drawbacks. If you plan on using JWT in your SPA, read more about the storage options.
Update the context store so the token gets saved and restored from local storage:
mutations: {
...
setJwtToken (state, jwtToken) {
state.jwtToken = jwtToken
if (jwtToken) window.localStorage.setItem('jwtToken', jwtToken)
else window.localStorage.removeItem('jwtToken')
}
},
actions: {
restoreContext ({ commit, getters, state }) {
const jwtToken = window.localStorage.getItem('jwtToken')
if (jwtToken) commit('setJwtToken', jwtToken)
return axios.get('account/context').then(res => {
commit('setProfile', res.data)
if (getters.isAuthenticated) return Vue.prototype.startSignalR()
})
},
...
}
Now authentication with JWT should work as expected, even after page reloads. Let’s wrap up by making sure we can connect to the SignalR hub when using JWT.
Using JWT with SignalR
By now, most of the heavy lifting has already been done. The server can authenticate users with a valid JWT token and the Vue application is able to login using the JWT scheme.
Allowing the SignalR hub to work with JWT is pretty straightforward. Remember the ForwardDefaultSelector, where one of the conditions was to look at the query string for a parameter named access_token?
We need to update the JwtAuthScheme, which by default only knows to look at the Authorization header, so it also looks at this parameter. Update the AddJwtBearer segment of the ConfigureServices method in the Startup class:
.AddJwtBearer(JWTAuthScheme, options =>
{
...
options.Events = new JwtBearerEvents
{
OnMessageReceived = ctx =>
{
if (ctx.Request.Query.ContainsKey("access_token")){
ctx.Token = ctx.Request.Query["access_token"];
}
return Task.CompletedTask;
}
};
});
The final part is for the client application to include this query string parameter as part of the SignalR connection when using JWT! First update all the calls to the startSignalR method made from the context store, so any current JWT token is provided:
Vue.prototype.startSignalR(state.jwtToken)
Then update the startSignalR method itself. We just need to include an accessTokenFactory property as part of the HubConnectionBuilder in case we received a non-empty token:
Vue.prototype.startSignalR = (jwtToken) => {
connection = new HubConnectionBuilder()
.withUrl(
`${Vue.prototype.$http.defaults.baseURL}/question-hub`,
jwtToken ? { accessTokenFactory: () => jwtToken } : null
)
.configureLogging(LogLevel.Information)
.build()
...
}
This way the HubConnectionBuilder will include the access_token query string parameter only when a valid token has been passed, which will only happen when users are authenticated using the JWT scheme!
And this concludes the tutorial. Your application should be fully functional regardless of whether you choose to use JWT or Cookies as the authentication scheme.
Conclusion
ASP.NET Core is flexible enough so you can implement authentication using different schemes in a way that’s transparent to the rest of the application.
True, the documentation is mostly geared towards using the default Identity implementation with Cookies, but the flexibility is there and relatively easy to find resources such as these blog posts that have been created by the community to fill the gap.
It is no wonder then that SignalR, built on top of ASP.NET Core, inherits this flexibility. Adding authentication to SignalR hubs and clients is a simple step once you have already added authentication to the rest of your application.
Finally, Vue and its ecosystem with libraries like Vuex, makes a great job at being flexible and extensible itself! As demonstrated in the article, adding cross cutting concerns like authentication can be added cleanly and with very little repercussion to most components other than the root ones!
As a final note, I understand there is a lot to process in the article, bringing together quite a few different tools in order to build a working application, all of it mixed with a hairy subject like authentication.
Don’t feel discouraged if it didn’t make complete sense the first time.
Take your time, read through the GitHub code, download and run the application and break it down into the pieces that solve specific problems!
This article was technically reviewed by Dobromir Nikolov.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Daniel Jimenez Garciais a passionate software developer with 10+ years of experience who likes to share his knowledge and has been publishing articles since 2016. He started his career as a Microsoft developer focused mainly on .NET, C# and SQL Server. In the latter half of his career he worked on a broader set of technologies and platforms with a special interest for .NET Core, Node.js, Vue, Python, Docker and Kubernetes. You can
check out his repos.