---
title: "Building Secure AI Agents with Microsoft Agent Framework and Auth0: User Authentication"
description: "Learn how to add user authentication to an AI agent built with Microsoft Agent Framework and C#: the first step to secure it and the first part of a new article series."
authors:
  - name: "Andrea Chiarelli"
    url: "https://auth0.com/blog/authors/andrea-chiarelli/"
date: "Aug 3, 2026"
category: "Developers,Tutorial,.NET"
tags: ["dotnet", "blazor", "ai", "ai agent", "security", "authentication"]
url: "https://auth0.com/blog/building-secure-ai-agents-with-microsoft-agent-framework-and-auth0-part-1-user-authentication/"
---

# Building Secure AI Agents with Microsoft Agent Framework and Auth0: User Authentication

Expense approval is one of those workflows that sounds simple but breaks down fast. Managers forget to check the queue. Employees submit incomplete reports. Approvals sit pending for days. An AI agent that actively monitors expense submissions, asks for missing information, and routes approvals without anyone having to remember to do it could solve this problem.

This is the first article in a four-part series where you'll build exactly that: an AI expense approval agent using the [Microsoft Agent Framework (MAF)](https://learn.microsoft.com/agent-framework/overview/) and Auth0. The agent runs as a C\# Blazor Web App in .NET 10, chats with managers about their direct reports' expenses, and gets smarter with each article. Here is the plan for this series:

* **Part 1 (this article)**: Set up the agent, connect Auth0 login via the Auth0 .NET template, and let it respond to questions about expense reports.  
* **Part 2**: Replace mock data with a vector database and add [Auth0 Fine-Grained Authorization](https://auth0.com/fine-grained-authorization) so managers only see their own direct reports' expenses.  
* **Part 3**: Give the agent a Gmail tool via [Auth0 Token Vault](https://auth0.com/features/token-vault) so it can email employees on the manager's behalf when a report is incomplete.  
* **Part 4**: Wire up [Client-Initiated Backchannel Authentication (CIBA)](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-initiated-backchannel-authentication-flow) so the agent sends a push notification when it needs the manager's approval, letting them approve or reject from their phone.

By the end of this first article, a manager can log in, start a conversation, and ask the agent to list pending expense reports. The agent responds using their identity. That's the foundation everything else builds on.

## What You'll Need

To build and configure the agent, you need:

* [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) installed on your machine  
* [Auth0 CLI](https://auth0.com/docs/deploy-monitor/auth0-cli) for template auto-registration  
* An Auth0 account (you can [sign up for a free one](https://a0.to/blog_signup))  
* A [GitHub personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) with model access  
* Basic familiarity with Blazor

## A Quick Look at the Microsoft Agent Framework

MAF is the successor to [Semantic Kernel](https://learn.microsoft.com/en-us/semantic-kernel/overview/) and [AutoGen](https://microsoft.github.io/autogen/stable//index.html). It wraps a language model with session management, tool-calling, and multi-agent orchestration. The core primitive you'll use here is `AIAgent`, which wraps any `IChatClient`-compatible inference service.

The two things you need to know right now are:

1. **Function tools** are C\# methods decorated with `[Description]` attributes. Call `AIFunctionFactory.Create()` method to expose them to the agent.  
2. **Sessions** keep conversation history across turns. Create one with `agent.CreateSessionAsync()`, pass it to every `RunAsync()` call, and serialize it between requests to survive stateless roundtrips.

That's enough to get started, so let's build our agent.

## Creating the Project with the Auth0 Template

The [Auth0 Templates for .NET](https://github.com/auth0/auth0-dotnet-templates) package lets you scaffold a Blazor Web App with Auth0 authentication already wired up. Install it first by running the following command in a terminal window:

```bash
dotnet new install Auth0.Templates
```

If you have the Auth0 CLI installed and are logged in, the template auto-registers your application in your Auth0 tenant (see [the `auth0blazor` template docs](https://github.com/auth0/auth0-dotnet-templates/blob/main/docs/auth0blazor.md) for more details). To create your project, run the following command, which will create the Blazor project in the `ExpenseAgent` folder:

```bash
dotnet new auth0blazor -o ExpenseAgent
```

If you're not using the Auth0 CLI, you can pass your credentials directly:

```bash
dotnet new auth0blazor -o ExpenseAgent \
  --domain your-tenant.auth0.com \
  --client-id your-client-id
```

The template generates a .NET solution with two .NET projects:

* A Blazor server project in the `ExpenseAgent` folder  
* A Blazor WASM project in the `ExpenseAgent.Client` folder.

Both projects come with:

* `Auth0.AspNetCore.Authentication` pre-configured in `Program.cs`  
* Login and logout endpoints  
* An `AuthorizeView` component and `AuthorizeRouteView` in `Routes.razor`  
* Your Auth0 domain and client ID already in `appsettings.json`

> To learn [how to set up a Blazor Web App with Auth0 from scratch, read this blog post](https://auth0.com/blog/auth0-authentication-blazor-web-apps/).

## Adding the Agent Packages

From the Blazor server project folder `ExpenseAgent`, add the packages for MAF and session caching by running the following commands:

```bash
dotnet add package Microsoft.Agents.AI
dotnet add package OpenAI
dotnet add package Microsoft.Extensions.AI.OpenAI
dotnet add package Microsoft.Extensions.Caching.Memory
```

Once the packages are added to your project, let's enable access to the AI model.

### Updating appsettings.json

The template already populated your Auth0 settings, so you don't need to add anything if you used the automatic registration.

This sample project uses [gpt-4o-mini](https://platform.openai.com/docs/models/gpt-4o-mini) via [GitHub Models](https://docs.github.com/en/github-models), which is free for development purposes. Add the settings highlighted below to `appsettings.json`:

```json
// ExpenseAgent/appsettings.json

{
  "Auth0": {
    "Domain": "YOUR_AUTH0_DOMAIN",
    "ClientId": "YOUR_CLIENT_ID"
  },
  //👇new settings
  "GitHubModels": {
    "Token": "YOUR_GITHUB_MODEL_TOKEN",
    "Model": "gpt-4o-mini"
  }
  //👆new settings
}
```

Replace the placeholder `YOUR_GITHUB_MODEL_TOKEN` with your [fine-grained personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) on your GitHub profile. Make sure to include the `Model` permission with `Access: Read-only`.

That's it! Your Blazor project now has the basic configuration it needs to become an AI agent.

## Building the Agent Tools

The agent's tools are the C# methods it can call during a conversation. Right now, there's one: `GetExpenseReports()`. In part 2, this method will query a vector database. For now, it returns hardcoded sample data so we can focus on getting the agent wired up.

Create an `Agent` folder in the `ExpenseAgent` folder and add an `ExpenseAgentTools.cs` file with the following code:

```c#
// ExpenseAgent/Agent/ExpenseAgentTools.cs

using System.ComponentModel;
using System.Text.Json;

namespace ExpenseAgent.Agent;

public class ExpenseAgentTools
{
    private readonly string _managerId;

    public ExpenseAgentTools(string managerId)
    {
        _managerId = managerId;
    }

    [Description("Get the list of expense reports submitted by the manager's direct reports.")]
    public string GetExpenseReports(
        [Description("Optional keyword to filter expense reports by description or merchant")] string? filter = null)
    {
        // Hardcoded sample data
        var reports = new[]
        {
            new {
                Id = "exp-001",
                Employee = "alice@example.com",
                Amount = 450.00m,
                Merchant = "Marriott Hotels",
                Justification = "Client dinner during NYC conference",
                SubmittedAt = "2026-06-10"
            },
            new {
                Id = "exp-002",
                Employee = "bob@example.com",
                Amount = 0m,
                Merchant = "Delta Airlines",
                Justification = "",
                SubmittedAt = "2026-06-12"
            },
            new {
                Id = "exp-003",
                Employee = "carol@example.com",
                Amount = 120.50m,
                Merchant = "",
                Justification = "Team lunch",
                SubmittedAt = "2026-06-14"
            }
        };

        var filtered = filter is null
            ? reports
            : reports.Where(r =>
                r.Merchant.Contains(filter, StringComparison.OrdinalIgnoreCase) ||
                r.Justification.Contains(filter, StringComparison.OrdinalIgnoreCase)).ToArray();

        return JsonSerializer.Serialize(filtered);
    }
}
```

Notice the `Description` attributes applied to the `GetExpenseReports()` method and its parameter. This is what the AI model will take into account to decide when to use this tool.

Also, notice that `exp-002` has no amount or justification, and `exp-003` has no merchant. Those gaps become important in part 3 of the series when the agent learns to detect them and email the employee.

Finally, you see that the class constructor expects the `managerId` parameter, whose value is assigned to the private variable `_managerId`. However, both are unused. Actually, they will be used in the next article to filter the data using FGA.

## Building the Agent Service

Now let's implement a service that creates and manages the MAF agent. This service owns session serialization so the Blazor component doesn't have to think about it.

Create the `ExpenseAgentService.cs` file in the `Agent` folder with the following content:

```c#
// ExpenseAgent/Agent/ExpenseAgentService.cs

using Microsoft.Extensions.AI;
using System.ClientModel;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.Caching.Distributed;
using OpenAI;

namespace ExpenseAgent.Agent;

public class ExpenseAgentService
{
    private readonly IConfiguration _config;
    private readonly IDistributedCache _cache;
    private AIAgent? _agent;

    public ExpenseAgentService(IConfiguration config, IDistributedCache cache)
    {
        _config = config;
        _cache = cache;
    }

    private AIAgent GetOrCreateAgent(ExpenseAgentTools tools)
    {
        if (_agent is not null) return _agent;

        var apiKey = new ApiKeyCredential(_config["GitHubModels:Token"] ?? throw new InvalidOperationException("Missing configuration: GitHubModels:Token. See the README for details."));
        var model = _config["GitHubModels:Model"] ?? "openai/gpt-4o-mini";
        var openAIOptions = new OpenAIClientOptions()
        {
            Endpoint = new Uri("https://models.github.ai/inference")
        };

        var systemPrompt = """
            You are an expense approval assistant. You help managers review expense reports
            submitted by their direct reports. When asked, retrieve and summarize the reports.
            Be concise and professional. Address the manager by their first name when you know it.
            Format the expenses as an unordered list.
            """;

        IChatClient chatClient = new OpenAIClient(apiKey, openAIOptions)
            .GetChatClient(model)
            .AsIChatClient();

        _agent = chatClient.AsAIAgent(
            instructions: systemPrompt,
            name: "ExpenseAgent",
            tools: [AIFunctionFactory.Create(tools.GetExpenseReports)]);

        return _agent;
    }

    public async Task<string> ChatAsync(
        string userId,
        string userMessage,
        string? userName,
        ExpenseAgentTools tools)
    {
        var agent = GetOrCreateAgent(tools);

        var sessionKey = $"agent-session:{userId}";
        var serializedSession = await _cache.GetStringAsync(sessionKey);

        AgentSession session = serializedSession is not null
            ? await agent.DeserializeSessionAsync(JsonDocument.Parse(serializedSession).RootElement)
            : await agent.CreateSessionAsync();

        var response = await agent.RunAsync(userMessage, session);

        var serializedState = await agent.SerializeSessionAsync(session);
        await _cache.SetStringAsync(sessionKey, serializedState.GetRawText(),
            new DistributedCacheEntryOptions
            {
                SlidingExpiration = TimeSpan.FromHours(1)
            });

        return response.Text;
    }
}
```

Sessions live in the distributed cache keyed by user ID, so a manager's conversation survives across Blazor re-renders and even a server restart: swap `AddDistributedMemoryCache()` for Redis later and nothing else changes. Notice `GetOrCreateAgent()` takes tools as a parameter rather than a constructor dependency: that's what lets part 2 swap in manager-scoped tools without touching this method's signature.

Now, let's register the `ExpenseAgentService` and the distributed memory cache by adding the following lines to the `Program.cs` file:

```c#
// ExpenseAgent/Program.cs

using ExpenseAgent.Client.Pages;
using ExpenseAgent.Components;
using Auth0.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Components.Authorization;
using ExpenseAgent.Agent;  //👈 new code

//...existing code...

//👇new code
builder.Services.AddDistributedMemoryCache();
builder.Services.AddScoped<ExpenseAgentService>();
//👆new code

builder.Services.AddHttpClient();

//...existing code...
```

Your agent service is ready.

## Building the Chat Component

The chat UI is a Blazor component that renders server-interactively. It injects `ExpenseAgentService` directly. No HTTP controller layer needed, because the component runs on the server.

In the `Components/Pages` folder, create a `Chat.razor` component with the following code:

```c#
// ExpenseAgent/Components/Pages/Chat.razor

@page "/chat"
@attribute [Authorize]
@rendermode InteractiveServer

@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using ExpenseAgent.Agent

@inject ExpenseAgentService AgentService

<PageTitle>Expense Agent</PageTitle>

<h2>Expense Agent</h2>

<AuthorizeView>
    <Authorized Context="auth">
        <p class="text-muted">Logged in as <strong>@auth.User.Identity?.Name</strong></p>
    </Authorized>
</AuthorizeView>

<div class="card mb-3" style="height: 350px; overflow-y: auto; padding: 12px;">
    @foreach (var msg in _messages)
    {
        <p><strong>@msg.Role:</strong> @msg.Text</p>
    }
    @if (_thinking)
    {
        <p class="text-muted"><em>Agent is thinking...</em></p>
    }
</div>

<div class="input-group">
    <input class="form-control"
           @bind="_inputText"
           @bind:event="oninput"
           @onkeydown="HandleKeyDown"
           placeholder="Ask about expense reports..."
           disabled="@_thinking" />
    <button class="btn btn-primary" @onclick="SendMessage" disabled="@_thinking">Send</button>
</div>

@code {
    [CascadingParameter]
    private Task<AuthenticationState>? AuthState { get; set; }

    private readonly List<(string Role, string Text)> _messages = new();
    private string _inputText = "";
    private string? _userId;
    private string? _userName;
    private bool _thinking;

    protected override async Task OnInitializedAsync()
    {
        if (AuthState is not null)
        {
            var state = await AuthState;
            _userId = state.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
            _userName = state.User.FindFirst("name")?.Value
                ?? state.User.FindFirst(System.Security.Claims.ClaimTypes.GivenName)?.Value;
        }
    }

    private async Task SendMessage()
    {
        var text = _inputText.Trim();
        if (string.IsNullOrEmpty(text) || _userId is null) return;

        _inputText = "";
        _messages.Add(("You", text));
        _thinking = true;

        try
        {
            var tools = new ExpenseAgentTools(_userId);
            var reply = await AgentService.ChatAsync(_userId, text, _userName, tools);
            _messages.Add(("Agent", reply));
        }
        finally
        {
            _thinking = false;
        }
    }

    private async Task HandleKeyDown(KeyboardEventArgs e)
    {
        if (e.Key == "Enter") await SendMessage();
    }
}
```

`OnInitializedAsync()` pulls the manager's Auth0 user ID and display name from the authentication state. Those are the two pieces of identity `ChatAsync()` needs on every turn.

`@rendermode InteractiveServer` activates the Blazor SignalR circuit for this component, enabling reactive UI updates like the "thinking" indicator.

The `Authorize` attribute redirects unauthenticated users to login before the component renders.

### Updating Routes.razor

The template generates `Components/Routes.razor`. Make sure it uses `AuthorizeRouteView` so the `Authorize` attribute on the chat component takes effect:

```html
@* ExpenseAgent/Components/Routes.razor *@

<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }" NotFoundPage="typeof(Pages.NotFound)">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="@routeData"
                            DefaultLayout="@typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>
```

### Updating the Navigation

To keep things simple, we won't redesign the entire Blazor application user interface, which showcases the classic functionality of the Blazor templates. Instead, we will add a link to the chat page in `Components/Layout/NavMenu.razor`, as shown below:

```html
@* ExpenseAgent/Components/Layout/NavMenu.razor *@

@* ...existing code... *@

<div class="nav-item px-3">
    <NavLink class="nav-link" href="chat">
        <span class="bi bi-chat-dots-fill" aria-hidden="true"></span> Chat
    </NavLink>
</div>

@* ...existing code... *@
```

## Running the Application

Let's run the application you've created so far by typing the following command:

```bash
dotnet run
```

Navigate to the URL shown in your terminal (mine is `https://localhost:7099`). You'll see the app with a Login link in the navigation bar. Click it and you're redirected to Auth0's Universal Login page. After signing in, navigate to `/chat`.

Try asking: "*Show me the pending expense reports.*"

The agent calls `GetExpenseReports()`, formats the three sample reports, and responds as shown in the following screenshot:

![AI agent show the list of expense report](https://images.ctfassets.net/23aumh6u8s0i/49JCiHNYACtLOSgGseXOQr/16af5c2e3678b6d831c804f66a1b608d/ai-agent-showing-expenses.png)

Again, to keep things simple, we're not taking care of the output format. We're just focusing on the core behavior: the interaction with the AI model and use of the tool.

Ask a follow-up: "*Which ones are missing information?*"

Because the session persists across component renders, the agent knows what reports it just retrieved and can reason about them without another tool call. The output will seem like the one shown in the following image:

![AI agent shows expenses with missing data](https://images.ctfassets.net/23aumh6u8s0i/5wH2lnLuaqGxcCggrE1hXt/b2918ab19f83be481e0a64a4cd9454aa/ai-agent-showing-expenses-with-missing-data.png)

## What the Agent Knows About the User

The agent knows two things about the logged-in manager:

1. Their **Auth0 user ID** (the [`sub` claim](https://auth0.com/docs/secure/tokens/id-tokens), e.g., `auth0|64abc...`): this identifier will be used to look up authorized data in part 2.  
2. Their **display name**: it can be passed to the agent which can address them by name.

In part 2, the user ID becomes the key that [Auth0 FGA](https://auth0.com/fine-grained-authorization) uses to decide which expense reports this manager can see. The identity flows through the whole system, not just the login screen.

## Where This Leaves Us

You now have a working AI expense agent that:

* Authenticates managers via Auth0, set up in minutes with the Auth0 .NET template  
* Runs in a Blazor Web App with server-interactive rendering  
* Maintains multi-turn conversation state across component re-renders  
* Calls a tool to retrieve expense reports

The sample data is hardcoded, but in the next article you will store real expense reports in a vector database, write Auth0 FGA tuples that define which managers can see which expenses, and update the `GetExpenseReports()` tool to filter results before they reach the LLM.