Skip to main content

How can I handle token expiration and automatic token renewal in a Vue 3 SPA and a .NET Core API using Entra ID authentication?

---
title: How to handle token expiration in a Vue 3 SPA and .NET Core API using Entra ID
subtitle: How can I handle token expiration and automatic token renewal in a Vue 3 SPA and a .NET Core API using Entra ID authentication?
author: Jon LaBelle
date: September 15, 2024
notoc: false
---

Handling token expiration and automatic token renewal in a Vue 3 SPA and a .NET
Core API using Entra ID (formerly Azure AD) involves configuring the MSAL
library to manage tokens and ensuring your API is set up to validate them.
Here's a step-by-step guide:

### Step 1: Register Applications in Entra ID

1. **Register the API**:

   - Go to the Azure portal.
   - Navigate to "Azure Active Directory" > "App registrations" > "New registration".
   - Name your application (e.g., `MyApi`).
   - Set the "Supported account types" to "Accounts in this organizational directory only".
   - Click "Register".
   - Note the "Application (client) ID" and "Directory (tenant) ID".
   - Under "Expose an API", add a scope (e.g., `api://{client-id}/access_as_user`).

2. **Register the SPA**:

   - Go to "Azure Active Directory" > "App registrations" > "New registration".
   - Name your application (e.g., `MySpa`).
   - Set the "Supported account types" to "Accounts in this organizational directory only".
   - Under "Redirect URI", select "Single-page application (SPA)" and enter your SPA URL (e.g., `http://localhost:8080`).
   - Click "Register".
   - Note the "Application (client) ID".
   - Under "Authentication", add the redirect URI and enable "ID tokens" and "Access tokens".
   - Under "API permissions", add the scope you defined for the API (e.g., `api://{api-client-id}/access_as_user`).

### Step 2: Configure the .NET Core API

1. **Install Required Packages**:

   ```bash
   dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
   dotnet add package Microsoft.Identity.Web
   ```

2. **Configure Authentication in `appsettings.json`**:

   ```json
   {
     "AzureAd": {
       "Instance": "https://login.microsoftonline.com/",
       "Domain": "yourdomain.onmicrosoft.com",
       "TenantId": "your-tenant-id",
       "ClientId": "your-api-client-id",
       "Audience": "api://your-api-client-id"
     }
   }
   ```

3. **Configure Authentication in `Startup.cs`**:

   ```csharp
   using Microsoft.AspNetCore.Authentication.JwtBearer;
   using Microsoft.Identity.Web;

   public class Startup
   {
       public void ConfigureServices(IServiceCollection services)
       {
           services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
               .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));

           services.AddAuthorization();

           services.AddControllers();
       }

       public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
       {
           if (env.IsDevelopment())
           {
               app.UseDeveloperExceptionPage();
           }

           app.UseRouting();

           app.UseAuthentication();
           app.UseAuthorization();

           app.UseEndpoints(endpoints =>
           {
               endpoints.MapControllers();
           });
       }
   }
   ```

4. **Protect API Endpoints**:

   ```csharp
   using Microsoft.AspNetCore.Authorization;
   using Microsoft.AspNetCore.Mvc;

   [ApiController]
   [Route("api/[controller]")]
   public class ProtectedController : ControllerBase
   {
       [HttpGet]
       [Authorize]
       public IActionResult GetProtectedData()
       {
           return Ok("This is protected data.");
       }
   }
   ```

### Step 3: Configure the Vue 3 SPA

1. **Install Required Packages**:

   ```bash
   npm install @azure/msal-browser @azure/msal-vue
   ```

2. **Configure MSAL in Vue**:

   ```javascript
   // src/main.js
   import { createApp } from 'vue';
   import App from './App.vue';
   import { msalPlugin } from '@azure/msal-vue';
   import { PublicClientApplication } from '@azure/msal-browser';

   const msalConfig = {
     auth: {
       clientId: 'your-spa-client-id',
       authority: 'https://login.microsoftonline.com/your-tenant-id',
       redirectUri: 'http://localhost:8080'
     },
     cache: {
       cacheLocation: 'localStorage', // This is important to persist the tokens
       storeAuthStateInCookie: true
     }
   };

   const msalInstance = new PublicClientApplication(msalConfig);

   const app = createApp(App);
   app.use(msalPlugin, msalInstance);
   app.mount('#app');
   ```

3. **Add Login and Logout Methods**:

   ```javascript
   // src/components/Login.vue
   <template>
     <div>
       <button @click="login">Login</button>
       <button @click="logout">Logout</button>
     </div>
   </template>

   <script>
   import { useMsal } from '@azure/msal-vue';

   export default {
     setup() {
       const { instance } = useMsal();

       const login = () => {
         instance.loginPopup({
           scopes: ['api://your-api-client-id/access_as_user']
         });
       };

       const logout = () => {
         instance.logoutPopup();
       };

       return { login, logout };
     }
   };
   </script>
   ```

4. **Call Protected API Endpoints and Handle Token Refresh**:

   ```javascript
   // src/components/ProtectedData.vue
   <template>
     <div>
       <button @click="getProtectedData">Get Protected Data</button>
       <p>{{ data }}</p>
     </div>
   </template>

   <script>
   import { useMsal } from '@azure/msal-vue';
   import axios from 'axios';
   import { InteractionRequiredAuthError } from '@azure/msal-browser';

   export default {
     data() {
       return {
         data: ''
       };
     },
     setup() {
       const { instance, accounts } = useMsal();

       const getProtectedData = async () => {
         const account = accounts.value[0];
         try {
           const response = await instance.acquireTokenSilent({
             scopes: ['api://your-api-client-id/access_as_user'],
             account: account
           });

           const token = response.accessToken;
           const result = await axios.get('http://localhost:5000/api/protected', {
             headers: {
               Authorization: `Bearer ${token}`
             }
           });

           this.data = result.data;
         } catch (error) {
           if (error instanceof InteractionRequiredAuthError) {
             instance.acquireTokenPopup({
               scopes: ['api://your-api-client-id/access_as_user'],
               account: account
             }).then(response => {
               const token = response.accessToken;
               axios.get('http://localhost:5000/api/protected', {
                 headers: {
                   Authorization: `Bearer ${token}`
                 }
               }).then(result => {
                 this.data = result.data;
               });
             });
           }
         }
       };

       return { getProtectedData };
     }
   };
   </script>
   ```

### Summary

1. **Register Applications in Entra ID**: Register both the API and SPA applications in Azure AD.
2. **Configure .NET Core API**: Set up JWT authentication and protect API endpoints.
3. **Configure Vue 3 SPA**: Set up MSAL for authentication, persist tokens, and handle token refresh.

By following these steps, you can handle token expiration and automatic token renewal in your Vue 3 SPA and .NET Core API using Entra ID authentication.