Skip to main content

This setup provides a basic example of integrating a Vue 3 SPA with a .NET 8 API using Entra ID authentication, policy-based authorization, and custom authorization handlers. The Vue app uses Vite for building, Axios for API calls, Pinia for state management, and the Composition API for managing state and lifecycle.

---
title: .NET API, Vue 3, Entra ID, and Pinia Setup
subtitle: This setup provides a basic example of integrating a Vue 3 SPA with a .NET 8 API using Entra ID authentication, policy-based authorization, and custom authorization handlers. The Vue app uses Vite for building, Axios for API calls, Pinia for state management, and the Composition API for managing state and lifecycle.
author: Jon LaBelle
date: September 24, 2024
source: https://jonlabelle.com/snippets/view/markdown/net-api-vue-3-entra-id-and-pinia-setup
snippet: https://jonlabelle.com/snippets/view/markdown/net-api-vue-3-entra-id-and-pinia-setup
notoc: false
---

### Step-by-Step Plan

1. **Setup .NET 8 API with Entra ID Authentication**

   - Create a new .NET 8 Web API project.
   - Configure Entra ID authentication.
   - Implement policy-based authorization.
   - Create custom authorization handlers.

2. **Setup Vue 3 SPA with Vite**

   - Create a new Vue 3 project using Vite.
   - Install and configure Axios for API calls.
   - Setup Entra ID authentication in the Vue app.
   - Use Pinia for state management.
   - Implement Vue 3 Composition API for state and lifecycle management.

### .NET 8 API

#### 1. Create a new .NET 8 Web API project

```bash
dotnet new webapi -n EntraIDApi
cd EntraIDApi
```

#### 2. Configure Entra ID Authentication

Add the following NuGet packages:

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

Update `appsettings.json` with Entra ID configuration:

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

Update `Program.cs`:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdminRole", policy => policy.RequireRole("Admin"));
    options.AddPolicy("RequireUserRole", policy => policy.RequireRole("User"));
});

builder.Services.AddSingleton<IAuthorizationHandler, CustomRequirementHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, AnotherRequirementHandler>();

builder.Services.AddControllers();

var app = builder.Build();

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

app.MapControllers();

app.Run();
```

#### 3. Implement Policy-based Authorization and Custom Handlers

Create `CustomRequirement.cs`:

```csharp
public class CustomRequirement : IAuthorizationRequirement
{
    public string RequiredClaim { get; }

    public CustomRequirement(string requiredClaim)
    {
        RequiredClaim = requiredClaim;
    }
}
```

Create `CustomRequirementHandler.cs`:

```csharp
public class CustomRequirementHandler : AuthorizationHandler<CustomRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == requirement.RequiredClaim))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
```

Create `AnotherRequirementHandler.cs`:

```csharp
public class AnotherRequirementHandler : AuthorizationHandler<CustomRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomRequirement requirement)
    {
        // Additional logic for another requirement
        return Task.CompletedTask;
    }
}
```

Create a sample controller `WeatherForecastController.cs`:

```csharp
[Authorize(Policy = "RequireAdminRole")]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        // Your logic here
    }
}
```

### Vue 3 SPA

#### 1. Create a new Vue 3 project using Vite

```bash
npm create vite@latest vue3-spa --template vue
cd vue3-spa
npm install
```

#### 2. Install and configure Axios

```bash
npm install axios
```

Create `src/plugins/axios.js`:

```javascript
import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://localhost:5001',
  headers: {
    'Content-Type': 'application/json'
  }
});

apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('entra_id_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

export default apiClient;
```

#### 3. Setup Entra ID Authentication

Install MSAL:

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

Create `src/auth/auth.js`:

```javascript
import * as msal from '@azure/msal-browser';

const msalConfig = {
  auth: {
    clientId: 'your-client-id',
    authority: 'https://login.microsoftonline.com/your-tenant-id',
    redirectUri: 'http://localhost:3000'
  }
};

const msalInstance = new msal.PublicClientApplication(msalConfig);

export const login = async () => {
  const loginRequest = {
    scopes: ['user.read']
  };

  try {
    const loginResponse = await msalInstance.loginPopup(loginRequest);
    localStorage.setItem('entra_id_token', loginResponse.accessToken);
  } catch (error) {
    console.error(error);
  }
};
```

#### 4. Use Pinia for State Management

Install Pinia:

```bash
npm install pinia
```

Create `src/store/user.js`:

```javascript
import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null
  }),
  actions: {
    setUser(user) {
      this.user = user;
    }
  }
});
```

#### 5. Implement Vue 3 Composition API

Update `src/App.vue`:

```vue
<template>
  <div id="app">
    <button @click="login">Login</button>
    <div v-if="user">
      <p>Welcome, {{ user.name }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { login } from './auth/auth';
import { useUserStore } from './store/user';

const userStore = useUserStore();
const user = ref(null);

const loginHandler = async () => {
  await login();
  user.value = { name: 'User' }; // Fetch user details from API
  userStore.setUser(user.value);
};

onMounted(() => {
  user.value = userStore.user;
});
</script>
```

### Summary

This setup provides a basic example of integrating a Vue 3 SPA with a .NET 8 API using Entra ID authentication, policy-based authorization, and custom authorization handlers. The Vue app uses Vite for building, Axios for API calls, Pinia for state management, and the Composition API for managing state and lifecycle.