Secure Family Medical Info Hub
Build and lock down an ASP.NET Core API protecting family health records.
Introduction
30 Second Summary
Every family has medical information scattered across sticky notes, phone contacts, and memory. Medications, allergies, emergency doctor numbers. If you gathered all of that into one online hub, you would need to be absolutely certain that only the right people can access it.
In this project, you will build a full-stack Family Medical Info Hub that starts completely unprotected, then systematically lock it down with production-grade security layers: JWT authentication, role-based authorization, input validation, and CORS policies.
What You'll Build
A running local application where family members log in, browse their health records on a styled dashboard, and every unauthorized attempt to access someone else's data gets firmly rejected with an error code.
By the end of this project, you'll have:
- A secured ASP.NET Core API that returns medical data only when you present a valid JWT token. Anonymous requests receive a 401 Unauthorized response.
- Role-based access control that lets a Family Admin view all records while a regular Member sees only their own. Crossing that boundary returns a 403 Forbidden.
- A Blazor Server dashboard styled with Bootstrap where authenticated users browse medications, allergies, and doctor contacts behind protected routes.
- Secret Mission: Add refresh token rotation and a sliding-window rate limiter that returns 429 Too Many Requests when someone spams your API.
Are there any prerequisites?
This project assumes you have experience with C# and ASP.NET Core. You should already have Visual Studio and the .NET 9 SDK installed on your machine.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters. Family medical data is some of the most sensitive information you can store digitally, and how you protect it defines whether the app is useful or dangerous.
Install PostgreSQL and Create the Solution
Every piece of medical data your family stores needs a permanent, reliable home. PostgreSQL is the database that will hold this sensitive information securely on your local machine.
In this step, you will install PostgreSQL locally, then create a Visual Studio solution with two projects: a Blazor Server frontend and an ASP.NET Core Web API backend. This separation mirrors how production applications keep their UI and data layers apart.
In this step, get ready to:
- Install PostgreSQL 17 and verify it is running locally.
- Create a Visual Studio solution with a Blazor Web App and a Web API project.
- Define entity models and apply database migrations with Entity Framework Core.
Install PostgreSQL 17
PostgreSQL is an open-source relational database. You need version 17 installed locally so Entity Framework Core can create tables to store family medical records.
- Open your web browser and navigate to the PostgreSQL Windows download page:
https://www.postgresql.org/download/windows/
- Click the Download the installer link to go to the EDB interactive installer page.
- Download the installer for PostgreSQL 17 (the latest 17.x version) for Windows x86-64.
✔️ I already have PostgreSQL 17 installed
If you already have PostgreSQL 17 running on your machine, skip ahead to the verification step below.
ⓧ I have an older version of PostgreSQL
PostgreSQL supports side-by-side installations. Download and run the PostgreSQL 17 installer from the EDB page above. The new version will install alongside your existing one on a different port, or you can choose to upgrade in place.
- During the installation wizard, keep the default port 5432 if you want to replace your old installation, or pick a different port to run them side by side.
- Follow the installation steps in the next tab for the full walkthrough.
ⓧ I don't have PostgreSQL installed
Run the installer you just downloaded. The wizard will guide you through the setup.
- On the Select Components screen, keep all components checked: PostgreSQL Server, pgAdmin 4, Stack Builder, and Command Line Tools.
- On the Installation Directory screen, accept the default path.
- On the Password screen, set a superuser password. Remember this password. You will need it for your connection string later.
- On the Port screen, keep the default port 5432.
- On the Locale screen, accept the default locale.
- Click Next through the summary and wait for installation to complete.
- When the installer finishes, uncheck the Launch Stack Builder option and click Finish.
What does this install?
The EDB installer bundles the PostgreSQL server, pgAdmin (a graphical database management tool), and command-line tools like psql. You will use pgAdmin to verify the server is running and later to inspect your database tables.
- Open pgAdmin by pressing the Windows key, typing pgAdmin, and pressing Enter.
- When pgAdmin opens, it will ask for a master password. Set one and remember it.
- In the left sidebar, expand Servers. You should see a PostgreSQL 17 entry.
- Click on it. pgAdmin will ask for the superuser password you set during installation. Enter it.
- Confirm that the server icon shows a green arrow, indicating it is running.
Server not showing as running?
- Make sure you entered the correct superuser password from the installation wizard.
- Open Services (press the Windows key and type Services). Find postgresql-x64-17 and confirm its status is Running. If not, right-click and select Start.
Help me troubleshoot my PostgreSQL installation on Windows.
Create the Blazor Web App solution
Your solution will contain two projects. The Blazor Web App is the frontend that family members interact with. You will add the API project next.
- Switch back to Visual Studio.
- Click File in the top menu bar.
- Click New, then Project.
- In the search box, type Blazor Web App and select the Blazor Web App template (the one with the C# tag).
- Click Next.
- In the Project name field, enter FamilyMedHub.
- Choose a location on your machine for the solution, then click Next.
- Set Interactive render mode to Server.
- Set Authentication type to None.
- Click Create.
Visual Studio creates the solution and opens the Blazor Web App project. You should see FamilyMedHub in the Solution Explorer on the right.
Why Server interactive render mode?
Server mode runs your Blazor components on the server and sends UI updates to the browser over a real-time connection. This keeps sensitive logic (like authentication state and API calls) server-side, which fits a medical data app where you do not want secrets in the browser.
Add the Web API project and configure Entity Framework Core
The API project will hold your data models, database context, and all the endpoints that the Blazor frontend calls. Keeping API logic in its own project means you can deploy and secure it independently.
- In Solution Explorer, right-click the FamilyMedHub solution name (the top-level item).
- Click Add, then New Project.
- Search for ASP.NET Core Web API and select the template.
- Click Next.
- In the Project name field, enter FamilyMedHub.Api.
- Click Next, then Create to accept the defaults.
You should now see both FamilyMedHub and FamilyMedHub.Api listed in Solution Explorer.
- Install the PostgreSQL Entity Framework Core provider by opening the Package Manager Console: click Tools > NuGet Package Manager > Package Manager Console.
- In the Package Manager Console, make sure the Default project dropdown at the top is set to FamilyMedHub.Api.
- Install the Npgsql provider by running this command:
Install-Package Npgsql.EntityFrameworkCore.PostgreSQL -Version 9.0.4
- Install the EF Core tools package so you can run migrations:
Install-Package Microsoft.EntityFrameworkCore.Tools
You should see Successfully installed messages for both packages in the console output.
What is Npgsql.EntityFrameworkCore.PostgreSQL?
This NuGet package is the PostgreSQL database provider for Entity Framework Core. It translates your C# LINQ queries into PostgreSQL SQL statements. Version 9.0.4 is compatible with .NET 9 projects.
Now create the entity models that represent your medical data. Each model becomes a table in the database.
- In Solution Explorer, right-click the FamilyMedHub.Api project.
- Click Add > New Folder. Name it Models.
- Right-click the Models folder, click Add > Class. Name it FamilyMember.cs.
- Replace the file contents with this code:
namespace FamilyMedHub.Api.Models;
// Represents a family member who can log in and own medical records
public class FamilyMember
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public string Role { get; set; }
public ICollection<Medication> Medications { get; set; }
public ICollection<Allergy> Allergies { get; set; }
public ICollection<DoctorContact> DoctorContacts { get; set; }
}
What does this model represent?
- Each FamilyMember has login credentials (Email, PasswordHash) and a Role (Admin or Member) for authorization later.
- The ICollection properties are navigation properties. They tell Entity Framework that one family member can have many medications, allergies, and doctor contacts.
- Save FamilyMember.cs.
- Press Ctrl+Shift+B to build the solution. Confirm there are no errors in the Error List pane.
Seeing red squiggles on the collection types?
The compiler does not yet know about Medication, Allergy, or DoctorContact because you have not created those classes yet. These errors will resolve after you add the remaining models in the next step.
Help me fix build errors in my FamilyMember model.
- Right-click the Models folder again. Add a new class named Medication.cs with this code:
namespace FamilyMedHub.Api.Models;
// Represents a medication prescribed to a family member
public class Medication
{
public int Id { get; set; }
public string Name { get; set; }
public string Dosage { get; set; }
public int FamilyMemberId { get; set; }
public FamilyMember FamilyMember { get; set; }
}
- Add another class named Allergy.cs in the Models folder:
namespace FamilyMedHub.Api.Models;
// Represents an allergy recorded for a family member
public class Allergy
{
public int Id { get; set; }
public string Name { get; set; }
public string Severity { get; set; }
public int FamilyMemberId { get; set; }
public FamilyMember FamilyMember { get; set; }
}
- Add one more class named DoctorContact.cs in the Models folder:
namespace FamilyMedHub.Api.Models;
// Represents a doctor contact associated with a family member
public class DoctorContact
{
public int Id { get; set; }
public string DoctorName { get; set; }
public string Specialty { get; set; }
public string Phone { get; set; }
public int FamilyMemberId { get; set; }
public FamilyMember FamilyMember { get; set; }
}
What do FamilyMemberId and the navigation property do?
Each child model (Medication, Allergy, DoctorContact) has a FamilyMemberId foreign key linking it to a specific family member. The FamilyMember navigation property lets Entity Framework load the related parent object when needed.
- Save all three files.
- Press Ctrl+Shift+B to build. You should now see Build succeeded in the status bar with zero errors.
Build still failing?
- Confirm all four model files are inside the Models folder of the FamilyMedHub.Api project (not the Blazor project).
- Check that each file uses the namespace FamilyMedHub.Api.Models on the first line.
Help me fix model build errors in my ASP.NET Core project.
Next, create the database context that connects your models to PostgreSQL.
- Right-click the FamilyMedHub.Api project, click Add > New Folder. Name it Data.
- Right-click the Data folder, click Add > Class. Name it MedHubDbContext.cs.
- Replace the file contents with this code:
using Microsoft.EntityFrameworkCore;
using FamilyMedHub.Api.Models;
namespace FamilyMedHub.Api.Data;
// The database context that maps entity models to PostgreSQL tables
public class MedHubDbContext : DbContext
{
public MedHubDbContext(DbContextOptions<MedHubDbContext> options)
: base(options) { }
public DbSet<FamilyMember> FamilyMembers { get; set; }
public DbSet<Medication> Medications { get; set; }
public DbSet<Allergy> Allergies { get; set; }
public DbSet<DoctorContact> DoctorContacts { get; set; }
}
What is a DbContext?
The DbContext is Entity Framework Core's bridge between your C# code and the database. Each DbSet property becomes a table in PostgreSQL. When you query context.Medications, EF Core translates that into a SQL query against the Medications table.
Now configure the connection string so Entity Framework knows which PostgreSQL database to use.
- In Solution Explorer, expand FamilyMedHub.Api and open appsettings.json.
- Replace the entire file contents with this configuration:
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=FamilyMedHubDb;Username=postgres;Password=your-superuser-password-here"
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
- Replace your-superuser-password-here with the password you set during PostgreSQL installation.
What does this connection string do?
It tells Entity Framework Core to connect to PostgreSQL on localhost port 5432, and create a database called FamilyMedHubDb. The username postgres is the default superuser created by the installer.
Register the DbContext in the API's startup so the application knows how to create database connections.
- Open Program.cs in the FamilyMedHub.Api project.
- Add these using statements at the top of the file:
using Microsoft.EntityFrameworkCore;
using FamilyMedHub.Api.Data;
- Find the line var app = builder.Build(); and add this code directly above it:
// Register the database context with PostgreSQL
builder.Services.AddDbContext<MedHubDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
- Save Program.cs and appsettings.json.
- Press Ctrl+Shift+B to build. Confirm Build succeeded with zero errors.
Finally, create the database by running Entity Framework Core migrations.
- In the Package Manager Console at the bottom of Visual Studio, confirm the Default project dropdown is set to FamilyMedHub.Api.
- Create the initial migration by running:
Add-Migration InitialCreate
- You should see a Migrations folder appear in the FamilyMedHub.Api project with a timestamped migration file.
- Apply the migration to create the actual database tables by running:
Update-Database
What do these commands do?
- Add-Migration InitialCreate generates C# code describing how to create the database schema (tables, columns, foreign keys) based on your models.
- Update-Database runs that generated code against your PostgreSQL server, creating the FamilyMedHubDb database and all four tables.
Before you check the result, what do you expect to see if you open pgAdmin and look under Databases? Take a moment to predict.
- Switch to pgAdmin in your taskbar.
- In the left sidebar, right-click Databases under your PostgreSQL 17 server and click Refresh.
- You should see FamilyMedHubDb listed as a new database.
- Expand FamilyMedHubDb > Schemas > public > Tables. You should see four tables: Allergies, DoctorContacts, FamilyMembers, and Medications.
Database not appearing in pgAdmin?
- Check the Package Manager Console output for errors. A common issue is an incorrect password in your connection string.
- Make sure the PostgreSQL service is running (check Windows Services).
- Verify that appsettings.json has the correct password and that Port is set to 5432.
Help me fix EF Core migration errors connecting to PostgreSQL.
✔️ Awesome, I've got everything!
Great. Confirm both projects build without errors by pressing Ctrl+Shift+B one final time. You should see Build succeeded in the status bar.
ⓧ I'd like to double check the full code
Here are all the files you created in this step. Compare each one against your code.
namespace FamilyMedHub.Api.Models;
public class FamilyMember
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public string Role { get; set; }
public ICollection<Medication> Medications { get; set; }
public ICollection<Allergy> Allergies { get; set; }
public ICollection<DoctorContact> DoctorContacts { get; set; }
}
namespace FamilyMedHub.Api.Models;
public class Medication
{
public int Id { get; set; }
public string Name { get; set; }
public string Dosage { get; set; }
public int FamilyMemberId { get; set; }
public FamilyMember FamilyMember { get; set; }
}
namespace FamilyMedHub.Api.Models;
public class Allergy
{
public int Id { get; set; }
public string Name { get; set; }
public string Severity { get; set; }
public int FamilyMemberId { get; set; }
public FamilyMember FamilyMember { get; set; }
}
namespace FamilyMedHub.Api.Models;
public class DoctorContact
{
public int Id { get; set; }
public string DoctorName { get; set; }
public string Specialty { get; set; }
public string Phone { get; set; }
public int FamilyMemberId { get; set; }
public FamilyMember FamilyMember { get; set; }
}
using Microsoft.EntityFrameworkCore;
using FamilyMedHub.Api.Models;
namespace FamilyMedHub.Api.Data;
public class MedHubDbContext : DbContext
{
public MedHubDbContext(DbContextOptions<MedHubDbContext> options)
: base(options) { }
public DbSet<FamilyMember> FamilyMembers { get; set; }
public DbSet<Medication> Medications { get; set; }
public DbSet<Allergy> Allergies { get; set; }
public DbSet<DoctorContact> DoctorContacts { get; set; }
}
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=FamilyMedHubDb;Username=postgres;Password=your-superuser-password-here"
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
Your database is live and your solution structure is in place. Next up, you will build API endpoints that expose this medical data without any authentication, so you can see firsthand why security matters.
Build the Unprotected API
Your database schema is ready, but there is no way for a client to read or write data yet. You need API endpoints that expose the family medical records over HTTP.
In this step, you will build those endpoints deliberately without any authentication. This lets you see firsthand that anyone with the URL can read, modify, or delete private health records. That open vulnerability is the problem every security layer in the next steps will solve.
In this step, get ready to:
- Create CRUD controllers for FamilyMembers, Medications, and Allergies.
- Seed the database with sample family data.
- Call the API anonymously and confirm anyone can access all health records.
Create the FamilyMembers controller
Controllers handle incoming HTTP requests and return responses. The FamilyMembersController will expose full CRUD (Create, Read, Update, Delete) endpoints for family member data.
- In Solution Explorer, right-click the FamilyMedHub.Api project.
- Select Add > New Folder and name it Controllers.
- Right-click the Controllers folder, select Add > Class, and name it FamilyMembersController.cs.
- Replace the file contents with the following code for the GET endpoints:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using FamilyMedHub.Api.Data;
using FamilyMedHub.Api.Models;
namespace FamilyMedHub.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class FamilyMembersController : ControllerBase
{
private readonly MedHubDbContext _db;
public FamilyMembersController(MedHubDbContext db) { _db = db; }
// Returns all members with their medications and allergies
[HttpGet]
public async Task<IActionResult> GetAll() =>
Ok(await _db.FamilyMembers.Include(f => f.Medications).Include(f => f.Allergies).ToListAsync());
// Returns a single member by ID
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var member = await _db.FamilyMembers.FindAsync(id);
return member == null ? NotFound() : Ok(member);
}
What does this code do?
The [ApiController] and [Route("api/[controller]")] attributes tell ASP.NET Core this class handles HTTP requests at /api/familymembers.
The GetAll method uses .Include() to eagerly load each member's medications and allergies in a single query. GetById returns a single member or a 404 if the ID does not exist.
- Add the write endpoints (Create, Update, Delete) below the GetById method, still inside the class:
// Creates a new family member record
[HttpPost]
public async Task<IActionResult> Create(FamilyMember member)
{
_db.FamilyMembers.Add(member);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(GetById), new { id = member.Id }, member);
}
// Updates an existing member
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, FamilyMember member)
{
if (id != member.Id) return BadRequest();
_db.Entry(member).State = EntityState.Modified;
await _db.SaveChangesAsync();
return NoContent();
}
// Deletes a member by ID
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var member = await _db.FamilyMembers.FindAsync(id);
if (member == null) return NotFound();
_db.FamilyMembers.Remove(member);
await _db.SaveChangesAsync();
return NoContent();
}
}
What do the write endpoints do?
- Create adds a new record and returns a 201 with a Location header pointing to the new resource.
- Update overwrites an existing record. It returns 400 if the URL ID and body ID mismatch.
- Delete removes a record. It returns 404 if the record does not exist.
Notice there is no [Authorize] attribute anywhere. Any anonymous request will succeed.
- Save FamilyMembersController.cs.
- Build the solution by pressing Ctrl+Shift+B.
- Confirm the Output window shows Build succeeded with no errors.
Seeing build errors?
- Check that the using statements at the top match exactly. A missing using Microsoft.EntityFrameworkCore; causes Include and EntityState to be unrecognized.
- Verify the namespace matches your project structure: FamilyMedHub.Api.Controllers.
- Make sure the closing } brace closes the class after the Delete method.
Help me fix build errors in my FamilyMembersController.
Create the Medications and Allergies controllers
The Medications and Allergies controllers follow the same CRUD pattern as FamilyMembers, but operate on their respective entities.
- Right-click the Controllers folder, select Add > Class, and name it MedicationsController.cs.
- Replace the file contents with the following read endpoints:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using FamilyMedHub.Api.Data;
using FamilyMedHub.Api.Models;
namespace FamilyMedHub.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class MedicationsController : ControllerBase
{
private readonly MedHubDbContext _db;
public MedicationsController(MedHubDbContext db) { _db = db; }
// Returns all medications across all family members
[HttpGet]
public async Task<IActionResult> GetAll() => Ok(await _db.Medications.ToListAsync());
// Returns a single medication by ID
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var med = await _db.Medications.FindAsync(id);
return med == null ? NotFound() : Ok(med);
}
- Add the write endpoints below GetById, still inside the class:
// Creates a new medication
[HttpPost]
public async Task<IActionResult> Create(Medication med)
{
_db.Medications.Add(med);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(GetById), new { id = med.Id }, med);
}
// Updates an existing medication
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, Medication med)
{
if (id != med.Id) return BadRequest();
_db.Entry(med).State = EntityState.Modified;
await _db.SaveChangesAsync();
return NoContent();
}
// Deletes a medication by ID
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var med = await _db.Medications.FindAsync(id);
if (med == null) return NotFound();
_db.Medications.Remove(med);
await _db.SaveChangesAsync();
return NoContent();
}
}
- Save MedicationsController.cs and build with Ctrl+Shift+B to confirm no errors.
- Right-click the Controllers folder again, select Add > Class, and name it AllergiesController.cs.
- Replace the file contents with the following read endpoints:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using FamilyMedHub.Api.Data;
using FamilyMedHub.Api.Models;
namespace FamilyMedHub.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AllergiesController : ControllerBase
{
private readonly MedHubDbContext _db;
public AllergiesController(MedHubDbContext db) { _db = db; }
// Returns all allergies across all family members
[HttpGet]
public async Task<IActionResult> GetAll() => Ok(await _db.Allergies.ToListAsync());
// Returns a single allergy by ID
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var allergy = await _db.Allergies.FindAsync(id);
return allergy == null ? NotFound() : Ok(allergy);
}
- Add the write endpoints below GetById, still inside the class:
// Creates a new allergy record
[HttpPost]
public async Task<IActionResult> Create(Allergy allergy)
{
_db.Allergies.Add(allergy);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(GetById), new { id = allergy.Id }, allergy);
}
// Updates an existing allergy
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, Allergy allergy)
{
if (id != allergy.Id) return BadRequest();
_db.Entry(allergy).State = EntityState.Modified;
await _db.SaveChangesAsync();
return NoContent();
}
// Deletes an allergy by ID
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var allergy = await _db.Allergies.FindAsync(id);
if (allergy == null) return NotFound();
_db.Allergies.Remove(allergy);
await _db.SaveChangesAsync();
return NoContent();
}
}
- Save AllergiesController.cs and build with Ctrl+Shift+B.
- Confirm Build succeeded appears in the Output window with 0 errors.
Why no authentication on any controller?
This is intentional. You are building the API in its most vulnerable state so you can see exactly what is exposed before adding security. Every endpoint is accessible to anyone who knows the URL. That is the gap you will close in Step 3.
Seed the database and test in Swagger
Empty endpoints are not very convincing. You need sample data in the database so the vulnerability is visible when you call the API.
- Open the Package Manager Console in Visual Studio (Tools > NuGet Package Manager > Package Manager Console).
- Make sure the Default project dropdown is set to FamilyMedHub.Api.
- Seed sample data by running the following in the Package Manager Console:
// Run this snippet in Program.cs, after var app = builder.Build();
// Add it before app.Run()
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<MedHubDbContext>();
// Only seed if no data exists yet
if (!db.FamilyMembers.Any())
{
var admin = new FamilyMember
{
Name = "Sarah Johnson",
Email = "sarah@family.com",
PasswordHash = "placeholder",
Role = "Admin"
};
var member = new FamilyMember
{
Name = "Tom Johnson",
Email = "tom@family.com",
PasswordHash = "placeholder",
Role = "Member"
};
db.FamilyMembers.AddRange(admin, member);
db.SaveChanges();
}
}
What does this seeding code do?
- It creates a scoped service to access the database context.
- It checks whether any FamilyMembers already exist to avoid duplicating data on restarts.
- It inserts two members: Sarah (Admin role) and Tom (Member role).
- Now add medications and allergies for both members. Place this inside the same if (!db.FamilyMembers.Any()) block, after db.SaveChanges();:
// Add medications for each member
db.Medications.AddRange(
new Medication { Name = "Lisinopril", Dosage = "10mg daily", FamilyMemberId = admin.Id },
new Medication { Name = "Metformin", Dosage = "500mg twice daily", FamilyMemberId = member.Id }
);
// Add allergies for each member
db.Allergies.AddRange(
new Allergy { Name = "Penicillin", Severity = "Severe", FamilyMemberId = admin.Id },
new Allergy { Name = "Peanuts", Severity = "Moderate", FamilyMemberId = member.Id }
);
db.SaveChanges();
- Save Program.cs.
- Set FamilyMedHub.Api as the startup project by right-clicking it in Solution Explorer and selecting Set as Startup Project.
- Press F5 to run the API.
Your browser should open automatically. Navigate to /swagger in the address bar (the full URL will look like https://localhost:{port}/swagger).
Before you try anything, predict: will Swagger require you to log in or provide a token before showing data?
- In Swagger UI, expand the GET /api/familymembers endpoint.
- Click Try it out, then click Execute.
You will see a 200 response containing both family members, their medications, and their allergies. No login. No token. No permission check.
- Now expand GET /api/medications and click Try it out > Execute.
- Try POST /api/medications with this JSON body:
{
"name": "Fake Medication",
"dosage": "999mg",
"familyMemberId": 1
}
The POST returns 201 Created. You just wrote a fake medication record to someone's health file without authenticating.
This is the vulnerability
Anyone who knows the URL can read every family member's medications, allergies, and personal details. They can also create, modify, and delete records. In a real application, this would be a critical security failure exposing protected health information.
This is exactly the gap you will close in the next step with JWT authentication.
Swagger UI not loading?
- Make sure FamilyMedHub.Api is the startup project (bold text in Solution Explorer).
- Confirm your launchSettings.json has "launchUrl": "swagger" in the https profile.
- If you see a blank page, try navigating directly to the full path in your browser address bar.
Help me get Swagger UI working in my ASP.NET Core Web API project.
✔️ Awesome, I've got everything!
Great. Double check you have saved all three controller files and Program.cs.
ⓧ I'd like to double check the full code
Scroll up and confirm each of these pieces is present in your project, in order:
- FamilyMembersController.cs with the class declaration, GET endpoints, then POST/PUT/DELETE endpoints.
- MedicationsController.cs with the same CRUD pattern using Medication entity.
- AllergiesController.cs with the same CRUD pattern using Allergy entity.
- Program.cs seeding block that creates Sarah (Admin) and Tom (Member) with medications and allergies.
Your API is live and fully exposed. In the next step, you will add JWT authentication so that only users who prove their identity with a signed token can access these endpoints.
Add JWT Authentication
In the last step, you saw the problem firsthand. Anyone with your API's URL can read, modify, or delete private medical records.
JWT authentication fixes this by requiring every request to carry a signed token that proves the caller's identity. In this step, you will build login and registration endpoints that issue tokens, then lock down all medical data controllers so only authenticated users can access them.
In this step, get ready to:
- Create an AuthController with register and login endpoints that issue signed JWTs.
- Configure JWT Bearer authentication middleware in the API pipeline.
- Lock down all medical data controllers with the [Authorize] attribute.
Create the AuthController with register and login endpoints
Before the API can verify identity, it needs endpoints where users can create an account and receive a token. You will install two NuGet packages, create data transfer objects for the request bodies, then build the controller.
- Install the BCrypt.Net-Next NuGet package (for password hashing) by opening the Package Manager Console in Visual Studio and running:
Install-Package BCrypt.Net-Next
- Install the Microsoft.AspNetCore.Authentication.JwtBearer NuGet package by running:
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
- Confirm both packages installed successfully by checking the output pane shows "Successfully installed" for each.
Package install failed?
Make sure the Package Manager Console dropdown at the top is set to the FamilyMedHub.Api project, not the Blazor project.
Help me fix this NuGet install issue
- Create a new folder called DTOs inside the FamilyMedHub.Api project. Right-click the project in Solution Explorer, select Add then New Folder, and name it DTOs.
- Inside the DTOs folder, add a new class file called RegisterDto.cs with the following code:
namespace FamilyMedHub.Api.DTOs;
// Carries registration data from the request body
public class RegisterDto
{
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string Role { get; set; } = "Member";
}
- Add another class file in the DTOs folder called LoginDto.cs with this code:
namespace FamilyMedHub.Api.DTOs;
// Carries login credentials from the request body
public class LoginDto
{
public string Email { get; set; }
public string Password { get; set; }
}
What are DTOs?
A Data Transfer Object (DTO) defines the shape of incoming request data without exposing your database model directly. This keeps your API surface clean and prevents callers from setting fields they should not control (like Id or PasswordHash).
Now you need JWT configuration values for token signing. You will add the issuer and audience to appsettings.json and store the secret key securely using the .NET Secret Manager.
- Open appsettings.json in the FamilyMedHub.Api project and add a Jwt section with the issuer and audience values:
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=FamilyMedHubDb;Username=postgres;Password=your_password_here"
},
"Jwt": {
"Issuer": "FamilyMedHub",
"Audience": "FamilyMedHubUsers"
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
Why is the key not in appsettings.json?
The signing key is a secret. If you put it in appsettings.json, it would end up in source control where anyone could read it. The .NET Secret Manager stores development secrets outside your project folder so they never get committed.
- Initialize the Secret Manager for the API project by running this in the Package Manager Console (make sure FamilyMedHub.Api is the default project):
dotnet user-secrets init --project FamilyMedHub.Api
- Store a JWT signing key by running:
dotnet user-secrets set "Jwt:Key" "ThisIsADevelopmentKeyThatIsAtLeast32Chars!" --project FamilyMedHub.Api
- You should see a confirmation message stating the secret was saved successfully.
Command not recognized?
If the dotnet command is not found in the Package Manager Console, open a regular terminal (Developer Command Prompt or PowerShell) and run the commands from the solution folder instead.
Help me store JWT secrets in my project
With the configuration ready, create the AuthController. You will build it in two parts: the class structure with the Register method first, then the Login and token-generation logic.
- In the Controllers folder of FamilyMedHub.Api, add a new class file called AuthController.cs and paste the following code:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using FamilyMedHub.Api.Data;
using FamilyMedHub.Api.DTOs;
using FamilyMedHub.Api.Models;
namespace FamilyMedHub.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly MedHubDbContext _db;
private readonly IConfiguration _config;
public AuthController(MedHubDbContext db, IConfiguration config)
{
_db = db;
_config = config;
}
}
What does this code do?
- The class injects MedHubDbContext (to query users in the database) and IConfiguration (to read JWT settings from appsettings and Secret Manager).
- The [ApiController] attribute enables automatic model validation and binding.
- Inside the AuthController class (below the constructor's closing brace), add the Register method:
// Register a new family member with hashed password
[HttpPost("register")]
public async Task<IActionResult> Register(RegisterDto dto)
{
var member = new FamilyMember
{
Name = dto.Name,
Email = dto.Email,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(dto.Password),
Role = dto.Role
};
_db.FamilyMembers.Add(member);
await _db.SaveChangesAsync();
return Ok(new { message = "Registered successfully" });
}
What does this code do?
- BCrypt.Net.BCrypt.HashPassword() takes the plain-text password and produces a one-way hash. The original password is never stored.
- The Role defaults to "Member" from the DTO, so callers can optionally specify "Admin" during registration.
- Below the Register method (still inside the class), add the Login method and the private GenerateJwt helper:
// Validate credentials and return a signed JWT
[HttpPost("login")]
public async Task<IActionResult> Login(LoginDto dto)
{
var member = await _db.FamilyMembers
.FirstOrDefaultAsync(m => m.Email == dto.Email);
if (member == null || !BCrypt.Net.BCrypt.Verify(dto.Password, member.PasswordHash))
return Unauthorized();
var token = GenerateJwt(member);
return Ok(new { token });
}
// Build a JWT with the user's ID, email, and role
private string GenerateJwt(FamilyMember member)
{
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, member.Id.ToString()),
new Claim(ClaimTypes.Email, member.Email),
new Claim(ClaimTypes.Role, member.Role)
};
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
What does this code do?
- BCrypt.Net.BCrypt.Verify() compares the submitted password against the stored hash without ever decrypting it.
- GenerateJwt builds a token containing three claims: the user's ID, email, and role. The token expires in one hour and is signed with the secret key from Secret Manager.
- The signed token is returned as JSON so the caller can attach it to future requests.
- Save AuthController.cs.
- Run the FamilyMedHub.Api project (press Ctrl+F5 or click the green play button).
- In the Swagger UI, expand POST /api/Auth/register and click Try it out.
- Enter this request body and click Execute:
{
"name": "Test User",
"email": "test@family.com",
"password": "SecurePass123",
"role": "Admin"
}
- You should see a 200 OK response with {"message": "Registered successfully"}.
- Now test login. Expand POST /api/Auth/login, click Try it out, and enter:
{
"email": "test@family.com",
"password": "SecurePass123"
}
- You should see a 200 OK response containing a token field with a long encoded string. That is your signed JWT.
Getting a 500 error on register or login?
Check the Output window in Visual Studio for the full exception. Common causes are a missing Jwt:Key secret (re-run the dotnet user-secrets set command) or a typo in the appsettings.json section name.
Help me debug this 500 error
Configure JWT Bearer authentication and add [Authorize]
The auth endpoints work, but the medical data controllers are still wide open. You need to tell the middleware to validate incoming tokens and reject requests without one.
- Open Program.cs in the FamilyMedHub.Api project.
- Add the following authentication configuration after the existing builder.Services calls (before var app = builder.Build();):
// Configure JWT authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
builder.Services.AddAuthorization();
What does this code do?
- AddAuthentication(JwtBearerDefaults.AuthenticationScheme) sets JWT Bearer as the default authentication scheme.
- TokenValidationParameters tells the middleware to check the token's issuer, audience, signing key, and expiration on every request.
- If any check fails, the middleware rejects the request before it reaches your controller.
- In the same Program.cs, find the middleware section (after var app = builder.Build();) and add UseAuthentication before UseAuthorization:
app.UseAuthentication();
app.UseAuthorization();
Middleware order matters
UseAuthentication() must come before UseAuthorization(). Authentication identifies the caller. Authorization then checks whether that caller has permission.
Now lock down the medical data controllers so unauthenticated callers are rejected.
- Open FamilyMembersController.cs. Find this line at the top of the class:
[ApiController][Route("api/[controller]")]
- Add [Authorize] before it so the line reads:
[Authorize][ApiController][Route("api/[controller]")]
- Repeat the same change in MedicationsController.cs and AllergiesController.cs. Add [Authorize] before [ApiController] in each file.
- Add using Microsoft.AspNetCore.Authorization; at the top of each controller file if Visual Studio does not auto-add it.
- Save all three files.
Before you run this, what do you think will happen when you call GET /api/familymembers without a token?
- Run the API project again.
- In Swagger, expand GET /api/FamilyMembers, click Try it out, then Execute.
You should see a 401 Unauthorized response. The endpoint now rejects anonymous requests.
Still getting 200 OK without a token?
Make sure you added [Authorize] to the controller class (not just a single method) and that app.UseAuthentication() appears before app.UseAuthorization() in Program.cs. Restart the project after saving.
Help me fix my [Authorize] setup
Test the full authenticated flow
You have confirmed that unauthenticated requests fail. Now prove that a valid token grants access.
- In Swagger, call POST /api/Auth/login with the credentials you registered earlier:
{
"email": "test@family.com",
"password": "SecurePass123"
}
- Copy the token value from the response (the long string between the quotes).
- At the top of the Swagger page, click the Authorize button (the padlock icon).
- In the value field, type Bearer followed by the token you copied. Click Authorize, then Close.
- Call GET /api/FamilyMembers again.
You should now see a 200 OK response with your seeded family member data returned. The same endpoint that rejected you moments ago now trusts you because your request carries a valid, signed token.
What just happened?
The middleware read the Authorization: Bearer <token> header, validated the token's signature, issuer, audience, and expiration, then set the request's identity to the claims inside the token. The [Authorize] attribute confirmed an identity existed and let the request through.
Getting 401 even with the token?
Make sure you typed Bearer (with a space) before pasting the token. Also check that the Jwt:Issuer and Jwt:Audience values in appsettings.json match the values used when the token was generated.
Help me debug JWT validation failures
✔️ Awesome, I've got everything!
Great. Double check you have saved all files: AuthController.cs, RegisterDto.cs, LoginDto.cs, appsettings.json, Program.cs, and the three controllers with [Authorize].
ⓧ I'd like to double check the full code
Scroll up and confirm each code block is present in your project in order:
- DTOs/RegisterDto.cs with 4 properties (Name, Email, Password, Role defaulting to "Member").
- DTOs/LoginDto.cs with 2 properties (Email, Password).
- appsettings.json with the Jwt section containing Issuer and Audience.
- AuthController.cs with the class structure, Register method, Login method, and GenerateJwt helper.
- Program.cs with AddAuthentication, AddJwtBearer, AddAuthorization, UseAuthentication(), and UseAuthorization().
- [Authorize] added to FamilyMembersController, MedicationsController, and AllergiesController.
Your API is no longer a free-for-all. Every medical data request now requires proof of identity. Next up, you will add role-based authorization so a regular family member cannot access another member's records.
Add Authorization, Validation, and CORS
Your API now requires a valid JWT to respond at all. That is a huge improvement over the wide-open state you witnessed in Step 2.
But authentication alone is not enough. A logged-in family member can still read every other member's medications. In this step you add three more security layers: role-based authorization that controls who sees what, input validation that rejects garbage data, and a CORS policy that restricts which frontend origins can call the API.
In this step, get ready to:
- Define authorization policies and enforce them on controllers so only Admins can view all family records.
- Add Data Annotations to models and DTOs so the API rejects invalid input with a 400 Bad Request.
- Configure a named CORS policy that allows only the Blazor frontend origin to call the API.
Add role-based authorization policies
Authorization policies define named rules the framework evaluates before allowing a request through. You will create two policies: one that requires the Admin role, and one that simply requires an authenticated user.
- In Visual Studio, open Program.cs in the FamilyMedHub.Api project.
- Find the existing AddAuthentication call. Directly below it, add the authorization policies by pasting this code:
// Define role-based authorization policies
builder.Services.AddAuthorization(options =>
{
// Only users with the Admin role can access
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
// Any authenticated user can access
options.AddPolicy("MemberAccess", policy =>
policy.RequireAuthenticatedUser());
});
What do these policies do?
The AdminOnly policy checks the Role claim inside the JWT. If the token does not contain Admin, the framework returns 403 Forbidden before your controller code even runs.
The MemberAccess policy simply requires any valid authenticated user. You can apply it to endpoints where both roles are welcome.
- Now update the FamilyMembersController to use the new policy. Replace the entire controller content in FamilyMembersController.cs with this code:
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class FamilyMembersController : ControllerBase
{
private readonly MedHubDbContext _db;
public FamilyMembersController(MedHubDbContext db)
{
_db = db;
}
// Only Admins can view all family members
[HttpGet]
[Authorize(Policy = "AdminOnly")]
public async Task<IActionResult> GetAll()
{
return Ok(await _db.FamilyMembers
.Include(f => f.Medications)
.Include(f => f.Allergies)
.ToListAsync());
}
}
What changed here?
The [Authorize(Policy = "AdminOnly")] attribute on GetAll ensures only tokens with Role = Admin pass through. Any Member who calls this endpoint gets a 403.
- Add a second endpoint below GetAll in the same controller so members can retrieve their own data:
// Members can view only their own record
[HttpGet("my")]
public async Task<IActionResult> GetMine()
{
var memberId = int.Parse(
User.FindFirstValue(ClaimTypes.NameIdentifier));
var member = await _db.FamilyMembers
.Include(f => f.Medications)
.Include(f => f.Allergies)
.FirstOrDefaultAsync(f => f.Id == memberId);
return member == null ? NotFound() : Ok(member);
}
How does claim filtering work?
The User.FindFirstValue(ClaimTypes.NameIdentifier) line reads the user's ID straight from their JWT claims. The query then filters to only that member's data. No one can pass a different ID in the URL to see someone else's records.
- Save FamilyMembersController.cs and Program.cs.
- Run the API project (press F5 or click the green play button).
- In Swagger, call POST /api/auth/login with a Member account's credentials and copy the token.
- Click Authorize in Swagger, paste the Bearer token, and call GET /api/familymembers.
You should see a 403 Forbidden response. The Member role does not satisfy the AdminOnly policy.
Still getting 200 instead of 403?
- Check that you registered the user with Role = "Member" during registration. If the role claim says Admin, the policy will pass.
- Confirm AddAuthorization appears after AddAuthentication in Program.cs.
- Make sure you saved and restarted the project so the new policies take effect.
Help me debug why my authorization policy is not blocking requests.
- Now apply the same claim-filtering pattern to MedicationsController.cs. Replace the GetAll method so it returns only the current user's medications:
// Return only the authenticated user's medications
[HttpGet]
public async Task<IActionResult> GetAll()
{
var memberId = int.Parse(
User.FindFirstValue(ClaimTypes.NameIdentifier));
return Ok(await _db.Medications
.Where(m => m.FamilyMemberId == memberId)
.ToListAsync());
}
- Apply the same pattern to AllergiesController.cs. Replace its GetAll method:
// Return only the authenticated user's allergies
[HttpGet]
public async Task<IActionResult> GetAll()
{
var memberId = int.Parse(
User.FindFirstValue(ClaimTypes.NameIdentifier));
return Ok(await _db.Allergies
.Where(a => a.FamilyMemberId == memberId)
.ToListAsync());
}
Why filter inside the query?
Rather than fetching everything and discarding rows in memory, the Where clause is translated to SQL. The database only returns rows the user owns, which is faster and safer.
- Save both controllers and restart the API.
- In Swagger, call GET /api/medications with a Member's token. You should see only that member's medications, not the full database.
Seeing an empty array?
An empty array means the filter matched no rows for that user's ID. Make sure the seeded medication data has a FamilyMemberId that matches the ID of the user you logged in as.
Help me figure out why my medications query returns empty.
Add input validation with Data Annotations
Without validation, a caller could post an empty medication name or a 10,000-character allergy string. Data Annotations enforce constraints at the model-binding stage so invalid data never reaches your database.
- Open Models/Medication.cs and replace it with the annotated version:
using System.ComponentModel.DataAnnotations;
public class Medication
{
public int Id { get; set; }
[Required]
[StringLength(200)]
public string Name { get; set; }
[StringLength(100)]
public string Dosage { get; set; }
public int FamilyMemberId { get; set; }
public FamilyMember FamilyMember { get; set; }
}
- Open Models/Allergy.cs and replace it with:
using System.ComponentModel.DataAnnotations;
public class Allergy
{
public int Id { get; set; }
[Required]
[StringLength(200)]
public string Name { get; set; }
[Required]
[StringLength(50)]
public string Severity { get; set; }
public int FamilyMemberId { get; set; }
public FamilyMember FamilyMember { get; set; }
}
What do these annotations enforce?
The [Required] attribute rejects null or empty strings. [StringLength(200)] caps the maximum character count. When model binding fails these checks, ASP.NET Core's [ApiController] attribute automatically returns 400 Bad Request with error details.
- Open DTOs/LoginDto.cs and add annotations:
using System.ComponentModel.DataAnnotations;
public class LoginDto
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
public string Password { get; set; }
}
- Open DTOs/RegisterDto.cs and add annotations:
using System.ComponentModel.DataAnnotations;
public class RegisterDto
{
[Required]
public string Name { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[MinLength(8)]
public string Password { get; set; }
public string Role { get; set; } = "Member";
}
- Finally, add explicit model state checking to the Create methods in MedicationsController.cs. Find your [HttpPost] method and add this check at the very beginning of the method body:
[HttpPost]
public async Task<IActionResult> Create(Medication med)
{
// Reject invalid input before touching the database
if (!ModelState.IsValid)
return BadRequest(ModelState);
var memberId = int.Parse(
User.FindFirstValue(ClaimTypes.NameIdentifier));
med.FamilyMemberId = memberId;
_db.Medications.Add(med);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(GetAll), new { id = med.Id }, med);
}
- Add the same ModelState.IsValid check to the Create method in AllergiesController.cs.
- Save all files and restart the API.
- In Swagger, call POST /api/medications with an empty Name field (pass "name": "").
You should receive a 400 Bad Request with a validation error message indicating the Name field is required.
Still getting 200 or 201?
- Check that [Required] is on the Name property in Medication.cs. Without it, an empty string passes.
- Make sure your controller has the [ApiController] attribute. This attribute enables automatic model state validation.
Help me debug why my validation annotations are not rejecting bad input.
Configure CORS to restrict API access to the Blazor frontend
When your Blazor frontend calls the API, the browser sends a preflight request to check whether the API allows that origin. Without a CORS policy the browser blocks the request entirely. You will add a named policy that allows only your Blazor app's origin.
- Open appsettings.json in the API project and add a Cors section below the Jwt section:
"Cors": {
"BlazorOrigin": "https://localhost:7001"
}
- Return to Program.cs. Below the AddAuthorization call, add the CORS service configuration:
// Configure CORS to allow only the Blazor frontend
var blazorOrigin = builder.Configuration["Cors:BlazorOrigin"];
builder.Services.AddCors(options =>
{
options.AddPolicy("BlazorClient", policy =>
policy.WithOrigins(blazorOrigin)
.AllowAnyMethod()
.AllowAnyHeader());
});
- In the middleware pipeline section of Program.cs, add UseCors after UseRouting but before UseAuthentication. Your middleware order should look like this:
app.UseRouting();
app.UseCors("BlazorClient");
app.UseAuthentication();
app.UseAuthorization();
Why does middleware order matter?
Microsoft's documentation states that UseCors, UseAuthentication, and UseAuthorization must appear in exactly this order. Placing CORS after auth causes preflight requests to fail because the browser's OPTIONS request carries no token.
Why not allow all origins?
A wildcard CORS policy (AllowAnyOrigin) lets any website call your API. For a medical data application, restricting to only the known Blazor frontend origin prevents malicious sites from making authenticated requests on behalf of your users.
- Save appsettings.json and Program.cs.
- Restart the API project.
Before you test the final verification, predict: if you call the API from Swagger (which runs on the same origin as the API), will CORS block it?
- In Swagger, log in as an Admin account, add the Bearer token, and call GET /api/familymembers.
You should receive 200 OK with all family members. Swagger is not a cross-origin request, so CORS does not block it. The CORS restriction will activate when the Blazor frontend (running on a different port) calls the API in the next step.
Seeing a 500 error on startup?
- Check that the Cors:BlazorOrigin key exists in appsettings.json. A null origin string causes AddCors to throw at startup.
- Confirm UseCors("BlazorClient") is placed after UseRouting() and before UseAuthentication().
Help me fix a startup crash related to CORS configuration.
✔️ Awesome, I've got everything!
Great. Double check you have saved all files, then move on.
ⓧ I'd like to double check the full code
Scroll up and confirm each code block is present in the correct file. Here is a checklist:
- Program.cs: AddAuthorization with AdminOnly and MemberAccess policies, AddCors with BlazorClient policy, middleware in correct order.
- FamilyMembersController.cs: GetAll with [Authorize(Policy = "AdminOnly")] and GetMine filtering by claim.
- MedicationsController.cs: GetAll filtering by FamilyMemberId claim, Create with ModelState.IsValid check.
- AllergiesController.cs: Same pattern as Medications.
- Medication.cs and Allergy.cs: [Required] and [StringLength] annotations.
- LoginDto.cs and RegisterDto.cs: [Required], [EmailAddress], [MinLength(8)] annotations.
- appsettings.json: Cors section with BlazorOrigin set to https://localhost:7001.
Your API now enforces three distinct security layers. Admins can manage all records. Members see only their own data. Invalid input is rejected before reaching the database. And only your Blazor frontend can make cross-origin calls.
Next up, you will build the Blazor Server dashboard that authenticates against this secured API and displays medical data behind protected routes.
Build the Blazor Server Dashboard
Your API is fully secured with JWT authentication, role-based authorization, input validation, and CORS. But right now, the only way to interact with it is through Swagger or raw HTTP requests.
In this step, you will build a Blazor Server dashboard that gives your family a real login form, a health data display, and role-aware navigation. The frontend talks to the secured API using the JWT tokens you built in previous steps.
In this step, get ready to:
- Create a login page that authenticates against the API and stores the JWT token.
- Build a dashboard page that fetches and displays medications and allergies using Bootstrap cards.
- Add an admin-only page with role-based navigation that shows or hides links based on user claims.
Create the authentication state provider and login page
Before the Blazor app can call protected API endpoints, it needs a way to authenticate users and store their JWT tokens. You will create a custom AuthenticationStateProvider that holds the token in memory and exposes user claims to the rest of the app.
- Install the required NuGet packages in the FamilyMedHub Blazor project. Open the Package Manager Console by clicking View > Other Windows > Package Manager Console.
- Set the Default project dropdown to FamilyMedHub (not the API project).
- Install the JWT token handler by running:
Install-Package System.IdentityModel.Tokens.Jwt
- Install the Blazor authorization components by running:
Install-Package Microsoft.AspNetCore.Components.Authorization
What do these packages do?
System.IdentityModel.Tokens.Jwt provides JwtSecurityTokenHandler which reads and parses JWT tokens so your Blazor app can extract user claims (like role and ID) from the token string.
Microsoft.AspNetCore.Components.Authorization provides AuthenticationStateProvider, CascadingAuthenticationState, AuthorizeView, and the [Authorize] attribute for Blazor components.
- In the FamilyMedHub project, create a new folder called Models. Right-click the project in Solution Explorer, select Add > New Folder, and name it Models.
- Right-click the Models folder, select Add > Class, and name it ApiModels.cs.
- Add the following model classes that represent the data shapes returned by the API:
namespace FamilyMedHub.Models;
// Model for the login form
public class LoginModel
{
public string Email { get; set; } = "";
public string Password { get; set; } = "";
}
// Shape of the token response from /api/auth/login
public class TokenResponse
{
public string Token { get; set; } = "";
}
// Mirrors the API's medication data
public class Medication
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Dosage { get; set; } = "";
}
// Mirrors the API's allergy data
public class Allergy
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Severity { get; set; } = "";
}
// Mirrors the API's family member data
public class FamilyMember
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Email { get; set; } = "";
}
What does this code do?
These are simple Data Transfer Objects (DTOs) that match the JSON shapes the API returns. LoginModel holds the form fields. TokenResponse captures the JWT string from the login response.
The other classes mirror the medical data entities so the JSON deserializer can map API responses into strongly typed C# objects.
- Create another new folder in the FamilyMedHub project called Auth.
- Right-click the Auth folder, select Add > Class, and name it JwtAuthenticationStateProvider.cs.
- Add the following code that stores the JWT and exposes user claims to Blazor:
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
namespace FamilyMedHub.Auth;
public class JwtAuthenticationStateProvider
: AuthenticationStateProvider
{
private string _token;
// Store the token and notify Blazor auth state changed
public void SetToken(string token)
{
_token = token;
NotifyAuthenticationStateChanged(
GetAuthenticationStateAsync());
}
// Expose the raw token for API calls
public string GetToken() => _token;
public override Task<AuthenticationState>
GetAuthenticationStateAsync()
{
// No token means unauthenticated
if (string.IsNullOrEmpty(_token))
return Task.FromResult(new AuthenticationState(
new ClaimsPrincipal(new ClaimsIdentity())));
// Parse the JWT and extract claims
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(_token);
var identity = new ClaimsIdentity(jwt.Claims, "jwt");
return Task.FromResult(new AuthenticationState(
new ClaimsPrincipal(identity)));
}
}
What does this code do?
This class is the bridge between your JWT token and Blazor's authorization system. When a user logs in, SetToken stores the token and fires a notification so all AuthorizeView components re-evaluate.
GetAuthenticationStateAsync parses the JWT to extract claims (like role). If there is no token, it returns an unauthenticated identity, which causes [Authorize] pages to block access.
- Build the solution by pressing Ctrl+Shift+B.
The build should succeed with no errors. If you see errors about missing namespaces, confirm both NuGet packages installed in the correct project.
Seeing build errors?
- Check that the NuGet packages were installed in the FamilyMedHub project (not FamilyMedHub.Api). The Package Manager Console dropdown determines which project receives the package.
- Verify the namespace in JwtAuthenticationStateProvider.cs matches FamilyMedHub.Auth.
- help me fix build errors in my Blazor authentication setup
- Open Program.cs in the FamilyMedHub project (not the API project).
- Register the authentication state provider, HttpClient, and authorization services by adding this code after the var builder = WebApplication.CreateBuilder(args); line:
// Register JWT auth state provider
builder.Services.AddScoped<JwtAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(sp =>
sp.GetRequiredService<JwtAuthenticationStateProvider>());
// Register HttpClient to call the secured API
builder.Services.AddScoped(sp => new HttpClient
{
BaseAddress = new Uri("https://localhost:7000")
});
// Add Blazor authorization services
builder.Services.AddAuthorizationCore();
What does this code do?
The first two lines register JwtAuthenticationStateProvider both as itself (so pages can inject it directly to call GetToken()) and as the base AuthenticationStateProvider (so Blazor's auth system uses it automatically).
The HttpClient registration points to your API's URL. AddAuthorizationCore() enables the [Authorize] attribute and AuthorizeView components in Blazor.
Match your API port
The base address https://localhost:7000 must match your API project's launch URL. Check FamilyMedHub.Api/Properties/launchSettings.json for the actual HTTPS port and update it if different.
- Add these using statements at the top of the same Program.cs file:
using FamilyMedHub.Auth;
using Microsoft.AspNetCore.Components.Authorization;
- Open Components/App.razor in the FamilyMedHub project.
- Wrap the existing <Router> element with <CascadingAuthenticationState> so all components can access the current user's auth state:
<CascadingAuthenticationState>
<Router AppAssembly="typeof(Program).Assembly">
<!-- existing Found/NotFound content -->
</Router>
</CascadingAuthenticationState>
What does CascadingAuthenticationState do?
This wrapper provides the current user's authentication state as a cascading parameter to every component in the app. Without it, AuthorizeView and [Authorize] attributes would not know whether the user is logged in.
- Build the solution with Ctrl+Shift+B to confirm everything compiles.
You should see a successful build with no errors. Now you are ready to create the login page.
- In the Components/Pages folder, right-click and select Add > Razor Component. Name it Login.razor.
- Replace the file contents with the following login form:
@page "/login"
@using FamilyMedHub.Auth
@using FamilyMedHub.Models
@using System.Net.Http.Headers
<h3>Login</h3>
<EditForm Model="loginModel" OnValidSubmit="HandleLogin">
<DataAnnotationsValidator />
<div class="mb-3">
<label>Email</label>
<InputText class="form-control"
@bind-Value="loginModel.Email" />
</div>
<div class="mb-3">
<label>Password</label>
<InputText type="password" class="form-control"
@bind-Value="loginModel.Password" />
</div>
<button type="submit" class="btn btn-primary">
Login</button>
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="alert alert-danger mt-2">
@errorMessage</div>
}
</EditForm>
- Below the </EditForm> closing tag, add the @code block that handles the login logic:
@code {
private LoginModel loginModel = new();
private string errorMessage = "";
[Inject] private HttpClient Http { get; set; }
[Inject]
private JwtAuthenticationStateProvider AuthProvider { get; set; }
[Inject] private NavigationManager Nav { get; set; }
private async Task HandleLogin()
{
// Call the API login endpoint
var response = await Http.PostAsJsonAsync(
"/api/auth/login", loginModel);
if (response.IsSuccessStatusCode)
{
// Store the JWT and navigate to dashboard
var result = await response.Content
.ReadFromJsonAsync<TokenResponse>();
AuthProvider.SetToken(result.Token);
Nav.NavigateTo("/dashboard");
}
else
{
errorMessage = "Invalid credentials.";
}
}
}
What does HandleLogin do?
When the form submits, HandleLogin sends the email and password to POST /api/auth/login. If the API returns success, it extracts the JWT from the response body, stores it in the JwtAuthenticationStateProvider, and navigates to the dashboard.
On failure, it displays an error message below the form. The [Inject] attribute tells Blazor's dependency injection to provide these services automatically.
- Save all files.
- Configure the solution to run both projects simultaneously. Right-click the solution in Solution Explorer and select Configure Startup Projects.
- Choose Multiple startup projects and set both FamilyMedHub and FamilyMedHub.Api to Start.
- Click OK, then press F5 to run both projects.
- Navigate to https://localhost:7001/login in your browser.
You should see a Bootstrap-styled login form with Email and Password fields and a blue Login button.
- Enter the email and password for one of your seeded family members (the Admin account you created in Step 1).
- Click Login.
The page will attempt to navigate to /dashboard. You will see a "Not found" message because that page does not exist yet. That is expected and confirms your login flow is working.
Login showing Invalid credentials?
- Confirm the API project is running by checking that its console window is open and showing no errors.
- Verify the HttpClient base address in Program.cs matches the API's actual HTTPS port from launchSettings.json.
- Check that the email and password match what you registered or seeded in the database.
- help me debug why my Blazor login cannot reach the API
Create the dashboard page
Now that login works, you need a page to land on after authentication. The dashboard will call the API's medications and allergies endpoints with the stored JWT token and display the results in Bootstrap cards.
- In Components/Pages, add a new Razor Component called Dashboard.razor.
- Add the following markup that displays medications and allergies in a two-column layout:
@page "/dashboard"
@attribute [Authorize]
@using Microsoft.AspNetCore.Authorization
@using FamilyMedHub.Auth
@using FamilyMedHub.Models
@using System.Net.Http.Headers
<h3>My Health Dashboard</h3>
<div class="row">
<div class="col-md-6">
<h4>Medications</h4>
@foreach (var med in medications)
{
<div class="card mb-2">
<div class="card-body">
<h5>@med.Name</h5>
<p>Dosage: @med.Dosage</p>
</div>
</div>
}
</div>
<div class="col-md-6">
<h4>Allergies</h4>
@foreach (var allergy in allergies)
{
<div class="card mb-2">
<div class="card-body">
<h5>@allergy.Name</h5>
<p>Severity: @allergy.Severity</p>
</div>
</div>
}
</div>
</div>
- Below the closing </div> of the row, add the @code block:
@code {
private List<Medication> medications = new();
private List<Allergy> allergies = new();
[Inject] private HttpClient Http { get; set; }
[Inject]
private JwtAuthenticationStateProvider AuthProvider { get; set; }
protected override async Task OnInitializedAsync()
{
// Attach the JWT to every API request
Http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(
"Bearer", AuthProvider.GetToken());
// Fetch the authenticated user's medical data
medications = await Http
.GetFromJsonAsync<List<Medication>>(
"/api/medications");
allergies = await Http
.GetFromJsonAsync<List<Allergy>>(
"/api/allergies");
}
}
What does this code do?
The [Authorize] attribute blocks unauthenticated users from reaching this page. When the page loads, OnInitializedAsync attaches the JWT as a Bearer token header and fetches medications and allergies from the API.
The API filters results by the user's FamilyMemberId claim, so each user only sees their own medical data. The Bootstrap card classes give each record a clean, bordered layout.
- Save Dashboard.razor.
- Stop the running projects with Shift+F5 and press F5 to restart both.
- Navigate to https://localhost:7001/login and log in with your seeded credentials.
Before you see the result, predict: what data will the dashboard show if you log in as the seeded Member account?
After logging in, you should be redirected to the dashboard. You will see your family member's medications in cards on the left and allergies on the right. Each card displays the medication name with dosage, or the allergy name with severity level.
Dashboard showing empty cards?
- Check the browser developer console (F12) for network errors. A 401 status means the token is not being sent correctly.
- Verify that your seeded family member has medications and allergies in the database using pgAdmin.
- Confirm the API project is still running in its console window.
- help me figure out why my dashboard is not loading medical data
✔️ Awesome, I've got everything!
Great. Make sure you have saved Dashboard.razor and that data is loading correctly in the cards.
ⓧ I'd like to double check the full code
Scroll up and confirm each piece is present in Dashboard.razor in this order:
- The @page, @attribute [Authorize], and @using directives at the top.
- The two-column <div class="row"> markup with medications on the left and allergies on the right.
- The @code block with OnInitializedAsync setting the Bearer header and fetching both lists.
Add the admin-only page and role-based navigation
The dashboard shows individual medical data, but Family Admins need to see records across all members. You will create a page restricted by role and update the navigation menu so admin-only links are hidden from regular members.
- In Components/Pages, add a new Razor Component called AllFamilyMembers.razor.
- Add the following markup that fetches all family members and restricts access to admins:
@page "/admin/members"
@attribute [Authorize(Roles = "Admin")]
@using Microsoft.AspNetCore.Authorization
@using FamilyMedHub.Auth
@using FamilyMedHub.Models
@using System.Net.Http.Headers
@using Microsoft.AspNetCore.Components.Authorization
<h3>All Family Members</h3>
@if (members == null)
{
<p>Loading...</p>
}
else
{
@foreach (var m in members)
{
<div class="card mb-2">
<div class="card-body">
<h5>@m.Name</h5>
<p>@m.Email</p>
</div>
</div>
}
}
<AuthorizeView Roles="Admin">
<NotAuthorized>
<div class="alert alert-danger">
Access Denied. Admins only.
</div>
</NotAuthorized>
</AuthorizeView>
- Add the @code block below the markup:
@code {
private List<FamilyMember> members;
[Inject] private HttpClient Http { get; set; }
[Inject]
private JwtAuthenticationStateProvider AuthProvider { get; set; }
protected override async Task OnInitializedAsync()
{
// Attach JWT and fetch all family members
Http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(
"Bearer", AuthProvider.GetToken());
members = await Http
.GetFromJsonAsync<List<FamilyMember>>(
"/api/familymembers");
}
}
What does the Admin restriction do?
The [Authorize(Roles = "Admin")] attribute at the page level blocks any non-admin user from reaching this component. The AuthorizeView with NotAuthorized provides a styled fallback message for anyone who bypasses navigation.
The API endpoint GET /api/familymembers is also protected with the AdminOnly policy on the server side. Even if the UI restriction were bypassed, the API would still return 403. This is defense in depth.
- Open Components/Layout/NavMenu.razor in the FamilyMedHub project.
- Add navigation links for the dashboard and the admin page. Wrap the admin link in an AuthorizeView so it only appears for Admin users:
<div class="nav-item px-3">
<NavLink class="nav-link" href="dashboard">
Dashboard
</NavLink>
</div>
<AuthorizeView Roles="Admin">
<div class="nav-item px-3">
<NavLink class="nav-link" href="admin/members">
All Members
</NavLink>
</div>
</AuthorizeView>
What does AuthorizeView do here?
AuthorizeView conditionally renders its child content based on the user's claims. With Roles="Admin", the "All Members" link only appears when the logged-in user has the Admin role claim in their JWT. Regular family members will never see this link.
- Save all files.
- Stop and restart both projects with Shift+F5 then F5.
Before you test, predict: if you log in as a regular Member (not Admin), will you see the "All Members" link in the navigation?
- Navigate to https://localhost:7001/login and log in with the Admin account credentials.
- After the dashboard loads, look at the navigation menu on the left. You should see both Dashboard and All Members links.
- Click All Members. You should see cards listing all family members with their names and emails.
Now test the authorization boundary.
- Stop the projects, then restart and log in with a regular Member account.
- Check the navigation menu. The "All Members" link should be hidden.
- Try navigating directly to https://localhost:7001/admin/members in the URL bar.
You should see the "Access Denied. Admins only." message. This confirms role-based authorization works at both the navigation and page level.
Both accounts seeing All Members?
- Check that your seeded Member account has Role = "Member" (not "Admin") in the database. Use pgAdmin to verify the FamilyMembers table.
- Confirm that your AuthController Login endpoint includes the role as a claim in the JWT (e.g., ClaimTypes.Role).
- Verify the AuthorizeView Roles="Admin" in NavMenu.razor uses the exact role string stored in the database.
- help me debug why role-based authorization is not working in my Blazor app
Your Blazor Server dashboard is complete. You have a login flow, a data dashboard, and role-based navigation all talking to your secured API. Next up, the Secret Mission challenges you to add rate limiting and refresh token rotation for production-grade protection.
Secret mission
Add Rate Limiting and Refresh Token Rotation
Your API is authenticated and authorized, but it is still vulnerable to brute-force attacks and token theft. In this bonus challenge, you will add rate limiting to throttle abusive requests and implement refresh token rotation so access tokens can be short-lived without forcing users to re-login.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine. There are no cloud services or ongoing costs to worry about.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Local PostgreSQL 17 database FamilyMedHubDb (free, stored locally).
- PostgreSQL 17 server and pgAdmin (installed locally on port 5432).
- .NET Secret Manager entries for the JWT signing key (stored in your user profile).
- FamilyMedHub solution folder with API and Blazor projects.
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend the project soon.
- PostgreSQL continues running as a Windows service. It uses a small amount of memory but costs nothing.
- Your FamilyMedHubDb database persists locally and is ready whenever you start the API again.
- Your Secret Manager entries remain stored in your user profile and are available to the project automatically.
Pause - I'll come back to this later
Stop the PostgreSQL service to free up memory. Your database files and secrets remain intact for next time.
- Press the Windows key and type Services. Press Enter to open the Services app.
- Scroll down to find the postgresql-x64-17 service.
- Right-click it and select Stop.
- To restart later, right-click the same service and select Start.
Delete - I don't want to use this again
Remove all project resources completely. Follow each section below.
Drop the database
- Open pgAdmin.
- Expand Servers, then PostgreSQL 17, then Databases.
- Right-click FamilyMedHubDb and select Delete/Drop.
- Click Yes to confirm.
Clear your Secret Manager entries
- Open a terminal in the FamilyMedHub.Api project directory and run:
dotnet user-secrets clear
Delete the solution folder
- Close Visual Studio.
- Open File Explorer and navigate to the folder containing the FamilyMedHub solution.
- Delete the entire FamilyMedHub folder.
Uninstall PostgreSQL (optional)
- Press the Windows key and type Add or remove programs. Press Enter.
- Find PostgreSQL 17 in the list and click Uninstall.
- Select Entire application when prompted and click Next to remove PostgreSQL and pgAdmin.
Nice Work!
Nice Work!
You just built a full-stack Family Medical Info Hub secured with production-grade API security patterns. Your application protects sensitive health data using the same authentication and authorization techniques that real medical platforms rely on.
Here's what you accomplished:
- Built a multi-project ASP.NET Core solution with a Web API and a Blazor Server frontend communicating via HTTP with JWT tokens, backed by a PostgreSQL database managed through Entity Framework Core.
- Experienced the vulnerability of an unprotected API firsthand, then systematically locked it down with JWT Bearer authentication, role-based authorization policies, input validation, and CORS.
- Built a Bootstrap-styled Blazor dashboard with login, protected routes, and role-based navigation that shows each family member only the medical data they are authorized to see.
- Secret Mission: Added rate limiting middleware with fixed-window policies returning 429 responses, and refresh token rotation for production-grade token lifecycle management.
Ready to quiz yourself?