Build Your First Secure Full-Stack App: ASP.NET Core + React with JWT Auth

Build Your First Secure Full-Stack App: ASP.NET Core + React with JWT Auth

Tutorials

Welcome! If you're ready to dive into full-stack web development, connecting a backend API to a frontend user interface, you're in the right place. This tutorial will guide you step-by-step in building a basic secure application featuring:

  • A backend API using ASP.NET Core (C#).
  • A simple database using SQLite and Entity Framework Core.
  • User registration and login using JWT (JSON Web Tokens) for authentication.
  • Secured API endpoints requiring a valid token.
  • A frontend user interface using React.
  • Navigation and protected routes handled by TanStack Router.
  • Clear separation between public pages (like Home, Login, Register) and secured pages (like a Dashboard).

We'll set up everything using VS Code, a popular and powerful code editor for full-stack development.

This tutorial covers the core concepts and essential code. For the complete, ready-to-run project code, check out the full GitHub repository here: [Link to your GitHub Repository Here]

What You'll Need (Prerequisites):

  1. VS Code: Download and install from https://code.visualstudio.com/.
  2. .NET SDK: Download and install the latest version from https://dotnet.microsoft.com/download. This includes the .NET runtime and command-line tools.
  3. Node.js and npm: Download and install the latest LTS version from https://nodejs.org/. npm (Node Package Manager) is included with Node.js.

Setting Up Your Development Environment (VS Code)

Once you have VS Code, the .NET SDK, and Node.js installed, let's set up VS Code for a better full-stack experience:

  1. Open VS Code.
  2. Go to the Extensions view (Ctrl+Shift+X or Cmd+Shift+X).
  3. Search for and install these recommended extensions:
    • C# by Microsoft: Provides rich language support for C# and ASP.NET Core.
    • ES7+ React/Redux/GraphQL/React-Native snippets by dsznajder: Adds helpful code snippets for React.
    • Prettier - Code formatter by Prettier: Automatically formats your code for consistency.
    • ESLint by Dirk Baeumer: Integrates ESLint to find and fix problems in your JavaScript/React code.
  4. You might also find GitLens helpful for working with Git repositories.

That's it! VS Code is now ready to handle both your ASP.NET Core backend and your React frontend.


Part 1: Building the ASP.NET Core Backend (API)

This part will handle our data, user accounts, and issuing authentication tokens.

Step 1: Create the Project

Open your terminal or command prompt and navigate to the folder where you want to create your project. Run:

1dotnet new webapi -n MySecureApi
2cd MySecureApi
3code .
  • dotnet new webapi -n MySecureApi creates a new ASP.NET Core Web API project named MySecureApi.
  • cd MySecureApi moves you into the new project directory.
  • code . opens the project in VS Code.

Step 2: Install NuGet Packages

We need packages for SQLite database access using Entity Framework Core and for JWT authentication. Open the VS Code terminal (Ctrl+ or Cmd+). Make sure you are in the MySecureApi directory.

1dotnet add package Microsoft.EntityFrameworkCore.Sqlite
2dotnet add package Microsoft.EntityFrameworkCore.Design
3dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Step 3: Configure appsettings.json

We'll store our database connection string and JWT settings here. Open appsettings.Development.json (it's good practice to keep secrets out of the main appsettings.json for source control, though for a tutorial, placing them here is fine).

1// appsettings.Development.json
2{
3 "Logging": { /* ... */ },
4 "AllowedHosts": "*",
5 "ConnectionStrings": {
6 "DefaultConnection": "Data Source=mydatabase.db" // SQLite stores data in a file
7 },
8 "Jwt": {
9 "Key": "YOUR_SUPER_SECURE_RANDOM_KEY_AT_LEAST_16_CHARS_LONG", // !!! CHANGE THIS !!! Must be strong and secret
10 "Issuer": "YourApiIssuer", // e.g., your domain name
11 "Audience": "YourApiClient" // e.g., your frontend URL or app name
12 }
13}

Step 4: Create User Model

Create a folder named Models in the root of MySecureApi. Inside Models, create a file User.cs.

1// Models/User.cs
2using System.ComponentModel.DataAnnotations;
3
4namespace MySecureApi.Models
5{
6 public class User
7 {
8 [Key] // Makes Id the primary key
9 public int Id { get; set; }
10 [Required] // Makes Username a required field
11 public string Username { get; set; } = string.Empty; // Use string.Empty to avoid null
12 [Required]
13 public byte[] PasswordHash { get; set; } = new byte[0]; // Store hashed password
14 [Required]
15 public byte[] PasswordSalt { get; set; } = new byte[0]; // Store random salt
16 // Simple role for authorization
17 public string Role { get; set; } = "User"; // Default role for new users
18 }
19}

Step 5: Create Database Context

Entity Framework Core uses a DbContext to interact with the database. Create a folder Data and add a file DataContext.cs.

1// Data/DataContext.cs
2using Microsoft.EntityFrameworkCore;
3using MySecureApi.Models; // Make sure to use the correct namespace
4
5namespace MySecureApi.Data
6{
7 public class DataContext : DbContext
8 {
9 // Constructor to accept options (needed for dependency injection)
10 public DataContext(DbContextOptions<DataContext> options) : base(options) { }
11
12 // DbSet represents the Users table in the database
13 public DbSet<User> Users { get; set; }
14
15 // OnConfiguring is not typically needed if using AddDbContext in Program.cs
16 // but can be useful for simple setups or logging.
17 // If you configure the connection string in Program.cs, you can remove this method.
18 // protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
19 // {
20 // // Example: If not using appsettings.json or Program.cs config
21 // // optionsBuilder.UseSqlite("Data Source=mydatabase.db");
22 // }
23 }
24}

Step 6: Configure DB Context in Program.cs

Open Program.cs. Add the DataContext to the service collection and configure it to use SQLite with the connection string from appsettings.Development.json. Add the necessary using statements at the top.

1// Program.cs (Add these using statements at the top)
2using Microsoft.EntityFrameworkCore;
3using MySecureApi.Data; // Use your Data namespace
4
5var builder = WebApplication.CreateBuilder(args);
6
7// Add services to the container.
8
9// Configure DataContext to use SQLite
10builder.Services.AddDbContext<DataContext>(options =>
11{
12 // Get the connection string from appsettings.json
13 options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"));
14});
15
16// ... rest of Program.cs

Step 7: Create and Apply Database Migrations

Migrations translate your C# models into database schema. Open the VS Code terminal in the MySecureApi directory.

1dotnet ef migrations add InitialCreate
2dotnet ef database update
  • dotnet ef migrations add InitialCreate creates a snapshot of your current models and generates code to create the database schema (a migration file).
  • dotnet ef database update applies any pending migrations, creating the mydatabase.db file and the Users table.

You should now see a mydatabase.db file created in your project root.

Step 8: Configure JWT Authentication & Authorization

In Program.cs, add the services and middleware for JWT authentication and authorization. Add necessary using statements for JWT.

1// Program.cs (Add these using statements at the top)
2using System.Text; // Needed for Encoding
3using Microsoft.AspNetCore.Authentication.JwtBearer; // Needed for JWT Auth
4using Microsoft.IdentityModel.Tokens; // Needed for SymmetricSecurityKey
5
6// ... AddDbContext code ...
7
8// Configure JWT Authentication
9builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
10 .AddJwtBearer(options => {
11 options.TokenValidationParameters = new TokenValidationParameters
12 {
13 // Validate the signing key using the secret key from appsettings
14 ValidateIssuerSigningKey = true,
15 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
16
17 // Validate the issuer (who created the token)
18 ValidateIssuer = true,
19 ValidIssuer = builder.Configuration["Jwt:Issuer"],
20
21 // Validate the audience (who the token is for)
22 ValidateAudience = true,
23 ValidAudience = builder.Configuration["Jwt:Audience"],
24
25 // Validate the token's lifetime (expiration)
26 ValidateLifetime = true
27 };
28 });
29
30builder.Services.AddAuthorization(); // Add authorization services
31
32// ... other builder services ...
33
34var app = builder.Build();
35
36// ... app.UseHttpsRedirection(); etc ...
37
38// IMPORTANT: Authentication and Authorization middleware MUST be added here, AFTER routing and CORS
39app.UseAuthentication();
40app.UseAuthorization();
41
42// ... app.MapControllers(); etc ...

Step 9: Add CORS Configuration

Your React frontend will run on a different port (e.g., 3000 or 5173) than your backend. CORS (Cross-Origin Resource Sharing) must be configured to allow requests from the frontend to the backend. Add this to Program.cs.

1// Program.cs (Add using Microsoft.AspNetCore.Cors;)
2
3var MyAllowSpecificOrigins = "_myAllowSpecificOrigins"; // Define a policy name
4
5builder.Services.AddCors(options =>
6{
7 options.AddPolicy(MyAllowSpecificOrigins,
8 builder =>
9 {
10 // Replace with the actual origin(s) where your frontend will run
11 builder.WithOrigins("http://localhost:3000", "http://localhost:5173")
12 .AllowAnyHeader() // Allow all common headers
13 .AllowAnyMethod() // Allow GET, POST, PUT, DELETE, etc.
14 .AllowCredentials(); // Needed if you handle cookies/credentials (good practice)
15 });
16});
17
18var app = builder.Build();
19
20// ... app.UseHttpsRedirection();
21
22// Add CORS middleware AFTER routing, but BEFORE Auth/Auth
23app.UseCors(MyAllowSpecificOrigins);
24
25app.UseAuthentication();
26app.UseAuthorization();
27
28// ... app.MapControllers();

Step 10: Create Authentication Controller (AuthController.cs)

This controller will have endpoints for /api/auth/register and /api/auth/login. You'll need helper methods for securely handling passwords (hashing and verifying) and generating JWTs. For this tutorial, we'll show the controller structure; find the full helper method implementations in the linked repository.

Create a folder Controllers and add AuthController.cs.

1// Controllers/AuthController.cs
2using Microsoft.AspNetCore.Mvc;
3using MySecureApi.Data;
4using MySecureApi.Models;
5using Microsoft.IdentityModel.Tokens;
6using System.IdentityModel.Tokens.Jwt;
7using System.Security.Claims;
8using System.Text;
9using Microsoft.Extensions.Configuration;
10using System.Security.Cryptography; // For password hashing
11using System.Threading.Tasks;
12using Microsoft.EntityFrameworkCore; // Needed for .AnyAsync()
13
14namespace MySecureApi.Controllers
15{
16 // Define input models (DTOs - Data Transfer Objects)
17 public class UserRegistrationDto
18 {
19 public string Username { get; set; } = string.Empty;
20 public string Password { get; set; } = string.Empty;
21 }
22
23 public class UserLoginDto
24 {
25 public string Username { get; set; } = string.Empty;
26 public string Password { get; set; } = string.Empty;
27 }
28
29 [Route("api/[controller]")] // Base route is /api/auth
30 [ApiController]
31 public class AuthController : ControllerBase
32 {
33 private readonly DataContext _context;
34 private readonly IConfiguration _configuration;
35
36 public AuthController(DataContext context, IConfiguration configuration)
37 {
38 _context = context;
39 _configuration = configuration; // Access JWT settings from appsettings
40 }
41
42 // POST: api/auth/register
43 [HttpPost("register")]
44 public async Task<IActionResult> Register([FromBody] UserRegistrationDto request)
45 {
46 // Basic validation
47 if (request.Username == null || request.Password == null)
48 {
49 return BadRequest("Username and password are required.");
50 }
51
52 // Check if user already exists
53 if (await _context.Users.AnyAsync(u => u.Username == request.Username))
54 {
55 return BadRequest("Username already exists.");
56 }
57
58 // Create password hash and salt (SEE HELPER METHOD IN FULL REPO)
59 CreatePasswordHash(request.Password, out byte[] passwordHash, out byte[] passwordSalt);
60
61 var newUser = new User
62 {
63 Username = request.Username,
64 PasswordHash = passwordHash,
65 PasswordSalt = passwordSalt,
66 Role = "User" // Assign default role
67 };
68
69 _context.Users.Add(newUser);
70 await _context.SaveChangesAsync();
71
72 return Ok("User registered successfully!");
73 }
74
75 // POST: api/auth/login
76 [HttpPost("login")]
77 public async Task<IActionResult> Login([FromBody] UserLoginDto request)
78 {
79 // Basic validation
80 if (request.Username == null || request.Password == null)
81 {
82 return BadRequest("Username and password are required.");
83 }
84
85 // Find user by username
86 var user = await _context.Users.FirstOrDefaultAsync(u => u.Username == request.Username);
87
88 // Check if user exists and password is correct
89 if (user == null || !VerifyPasswordHash(request.Password, user.PasswordHash, user.PasswordSalt))
90 {
91 return Unauthorized("Invalid credentials."); // Use Unauthorized for login failures
92 }
93
94 // Generate JWT Token (SEE HELPER METHOD IN FULL REPO)
95 string token = CreateToken(user);
96
97 // Return the token to the client
98 return Ok(new { token = token });
99 }
100
101 // --- Helper Methods (See Full Repo for implementations) ---
102
103 // Creates a password hash and salt
104 private void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt) { /* ... */ }
105
106 // Verifies a plain password against a hash and salt
107 private bool VerifyPasswordHash(string password, byte[] passwordHash, byte[] passwordSalt) { /* ... */ }
108
109 // Creates a JWT token for a given user
110 private string CreateToken(User user) { /* ... */ }
111 }
112}

Step 11: Create Secured Controller (SecuredController.cs)

This controller will contain actions that can only be accessed by authenticated users (or users with specific roles). Create a file SecuredController.cs in the Controllers folder.

1// Controllers/SecuredController.cs
2using Microsoft.AspNetCore.Authorization; // Needed for [Authorize]
3using Microsoft.AspNetCore.Mvc;
4using System.Security.Claims; // Needed to access user claims
5
6// Apply [Authorize] at the controller level to protect all actions within it by default
7[Authorize]
8[Route("api/[controller]")] // Base route is /api/secured
9[ApiController]
10public class SecuredController : ControllerBase
11{
12 // GET: api/secured
13 // This action requires any authenticated user (because of the [Authorize] on the controller)
14 [HttpGet]
15 public IActionResult GetSecuredData()
16 {
17 // Access claims from the authenticated user's JWT token
18 var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; // Get the user ID claim (often added in CreateToken)
19 var userName = User.FindFirst(ClaimTypes.Name)?.Value; // Get the username claim
20 var userRole = User.FindFirst(ClaimTypes.Role)?.Value; // Get the role claim
21
22 return Ok($"Hello, {userName ?? 'Authenticated User'}! You accessed secured data. Your User ID: {userId}, Role: {userRole ?? 'None'}.");
23 }
24
25 // GET: api/secured/admin
26 // This action requires an authenticated user AND they must have the "Admin" role claim
27 [HttpGet("admin")]
28 [Authorize(Roles = "Admin")] // Override controller's Authorize to add role requirement
29 public IActionResult GetAdminData()
30 {
31 return Ok("This data is only accessible to users with the 'Admin' role!");
32 }
33
34 // Example of an action that requires *only* the controller-level auth (already implicit)
35 // [HttpGet("another-secured-endpoint")]
36 // public IActionResult AnotherSecuredAction()
37 // {
38 // return Ok("Another endpoint requiring authentication.");
39 // }
40}

Your backend API is now set up with user management, JWT issuance, and endpoint protection!


Part 2: Building the React Frontend with TanStack Router

Now, let's create the React application that consumes your secure API.

Step 12: Create the React Project

Open a new terminal or command prompt window (keep the backend terminal running). Navigate to the folder where you want to create your frontend project (e.g., alongside your MySecureApi folder). We'll use Vite for a fast setup.

1npm create vite@latest my-secure-app --template react
2cd my-secure-app
3npm install
4code .
  • npm create vite@latest my-secure-app --template react creates a new React project using Vite.
  • cd my-secure-app moves into the new project folder.
  • code . opens the frontend project in VS Code.

Step 13: Install npm Packages

We need Axios for making API calls and @tanstack/react-router for routing.

1npm install axios @tanstack/react-router

Step 14: Create API Client (api.js)

This file will contain our Axios instance configured to talk to the backend API and automatically attach the JWT token to requests. Create a file src/api.js.

1// src/api.js
2import axios from 'axios';
3
4// Create an Axios instance with your backend API base URL
5const api = axios.create({
6 // !! Update with the actual URL where your backend runs !!
7 // Usually https://localhost:7xxx or http://localhost:5xxx during development
8 baseURL: 'https://localhost:7100/api',
9});
10
11// --- Request Interceptor ---
12// This runs BEFORE each request leaves the client
13api.interceptors.request.use(
14 config => {
15 // Get the JWT token from local storage (or wherever you stored it)
16 const token = localStorage.getItem('jwtToken');
17
18 // If a token exists, add it to the Authorization header
19 if (token) {
20 config.headers['Authorization'] = `Bearer ${token}`;
21 }
22
23 // Return the modified config
24 return config;
25 },
26 error => {
27 // Handle request errors (e.g., network issues)
28 return Promise.reject(error);
29 }
30);
31
32// --- Response Interceptor (Optional but Recommended) ---
33// This runs AFTER a response is received but BEFORE it's passed to your code
34// Good for handling token expiration or other global errors
35api.interceptors.response.use(
36 response => {
37 return response; // Just pass through successful responses
38 },
39 error => {
40 // If the response is 401 Unauthorized (e.g., token expired)
41 if (error.response && error.response.status === 401) {
42 console.log("Unauthorized! Token might be expired or invalid. Logging out...");
43 // Clear token and redirect to login (implement logout logic)
44 localStorage.removeItem('jwtToken');
45 // You would typically trigger a state update or redirect here
46 // In a real app, this might trigger a global auth context update
47 // For now, we'll let the ProtectedRoute handle the redirect on the next render
48 }
49 return Promise.reject(error); // Propagate the error
50 }
51);
52
53
54export default api; // Export the configured instance

Step 15: Create Authentication Context (AuthContext.js)

We'll use React Context to manage the user's login state across the application. Create a file src/AuthContext.js.

1// src/AuthContext.js
2import React, { createContext, useState, useContext, useEffect } from 'react';
3import api from './api'; // Import your configured api client
4
5// Create the Context
6const AuthContext = createContext(null);
7
8// Create a Provider component
9export const AuthProvider = ({ children }) => {
10 // State to hold the user object (null if not logged in)
11 const [user, setUser] = useState(null);
12 // State to indicate if the initial loading check is complete
13 const [loading, setLoading] = useState(true);
14
15 // Check for existing token when the app starts
16 useEffect(() => {
17 const token = localStorage.getItem('jwtToken');
18 if (token) {
19 // --- IMPORTANT FOR A REAL APP ---
20 // In a real application, you should NOT assume the token is valid just because it exists.
21 // You should ideally call a secure backend endpoint (e.g., /api/auth/me or /api/auth/verify)
22 // that validates the token on the server and returns user details.
23 // For this boilerplate, we'll just set a basic user object if a token exists
24 // and rely on API calls failing (handled by interceptor) for invalid tokens.
25 try {
26 // Decode the token locally to get basic info like username (OPTIONAL & INSECURE for sensitive data)
27 // For a simple example, you can just set a placeholder
28 const base64Url = token.split('.')[1];
29 const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
30 const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
31 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
32 }).join(''));
33 const claims = JSON.parse(jsonPayload);
34
35 // Assuming username is stored in 'sub' or 'name' claim (check your backend CreateToken method)
36 const username = claims.sub || claims.name || 'AuthenticatedUser';
37 const role = claims.role || 'User'; // Assuming role is stored in 'role' claim
38
39 setUser({ username, role }); // Set user state based on token claims
40 console.log("Found token, attempting to use:", claims);
41
42 } catch (error) {
43 console.error("Failed to decode or process token:", error);
44 localStorage.removeItem('jwtToken'); // Clear invalid token
45 setUser(null);
46 }
47
48 }
49 setLoading(false); // Loading is complete
50 }, []); // Run only once on mount
51
52 // Login function: Calls backend, stores token, updates state
53 const login = async (username, password) => {
54 try {
55 const response = await api.post('/auth/login', { username, password });
56 const token = response.data.token;
57 localStorage.setItem('jwtToken', token); // Store the token
58
59 // --- IMPORTANT FOR A REAL APP ---
60 // Ideally, you'd get user details (like roles) from the login response
61 // For this boilerplate, we'll decode the token locally again
62 const base64Url = token.split('.')[1];
63 const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
64 const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
65 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
66 }).join(''));
67 const claims = JSON.parse(jsonPayload);
68
69 const userName = claims.sub || claims.name || 'AuthenticatedUser';
70 const role = claims.role || 'User'; // Assuming role is stored in 'role' claim
71
72 setUser({ username: userName, role }); // Update user state
73
74 console.log('Login successful, token stored.');
75 return true; // Indicate success
76 } catch (error) {
77 console.error('Login failed:', error.response?.data || error.message);
78 // Handle login errors (e.g., display error message in UI)
79 setUser(null); // Ensure user is null on failed login
80 localStorage.removeItem('jwtToken'); // Clear any potential partial tokens
81 return false; // Indicate failure
82 }
83 };
84
85 // Logout function: Removes token, clears state
86 const logout = () => {
87 localStorage.removeItem('jwtToken'); // Remove token from storage
88 setUser(null); // Clear user state
89 console.log('Logged out, token removed.');
90 // TanStack Router can be used for navigation here if needed:
91 // const navigate = useNavigate(); navigate('/login'); // Requires hook usage
92 };
93
94 // Provide the user state, login, and logout functions to children
95 return (
96 <AuthContext.Provider value={{ user, login, logout, loading }}>
97 {!loading && children} {/* Render children only after initial check */}
98 </AuthContext.Provider>
99 );
100};
101
102// Custom hook to easily access the auth context
103export const useAuth = () => {
104 const context = useContext(AuthContext);
105 if (context === null) {
106 // This means useAuth was called outside of the AuthProvider
107 throw new Error('useAuth must be used within an AuthProvider');
108 }
109 return context;
110};

Step 16: Define TanStack Router Routes

Create a folder src/routes. Inside this folder, create the following files. We'll use the lazy loading pattern (.lazy.jsx suffix) which is standard for components rendered by the router.

1// src/routes/root.lazy.jsx
2import { createLazyFileRoute, Outlet } from '@tanstack/react-router';
3import { AuthProvider } from '../AuthContext'; // Your Auth Context Provider
4
5// This is the root layout route. It wraps the entire application.
6export const Route = createLazyFileRoute('/')({
7 component: () => (
8 // Wrap the whole application with the AuthProvider so all routes can access auth context
9 <AuthProvider>
10 {/* Optional: Add a site-wide navigation menu here */}
11 <nav style={{ padding: '10px', borderBottom: '1px solid #ccc' }}>
12 <Link to="/">Home</Link> |{' '}
13 <Link to="/login">Login</Link> |{' '}
14 <Link to="/register">Register</Link> |{' '}
15 {/* Add conditional links for authenticated users later if desired */}
16 <Link to="/dashboard">Dashboard</Link> |{' '}
17 <Link to="/secured">Secured Data</Link>
18 <AuthStatusDisplay /> {/* Component to show login/logout button */}
19 </nav>
20
21 {/* The Outlet component renders the currently matched child route */}
22 <div style={{ padding: '10px' }}>
23 <Outlet />
24 </div>
25 </AuthProvider>
26 ),
27});
28
29
30// Create a simple component to display auth status and logout button
31// src/components/AuthStatusDisplay.jsx
32import React from 'react';
33import { useAuth } from '../AuthContext';
34import { Link, useNavigate } from '@tanstack/react-router';
35
36function AuthStatusDisplay() {
37 const { user, logout, loading } = useAuth();
38 const navigate = useNavigate();
39
40 if (loading) return <div>Loading Auth...</div>; // Or null
41
42 if (user) {
43 return (
44 <span>
45 | Logged in as: {user.username} (<button onClick={() => { logout(); navigate('/'); }}>Logout</button>)
46 </span>
47 );
48 } else {
49 // Links are already in the main nav, but you could add them here too
50 return <span>| Not logged in</span>;
51 }
52}
53
54
55// src/routes/index.lazy.jsx (Public Home Page)
56import { createLazyFileRoute } from '@tanstack/react-router';
57
58export const Route = createLazyFileRoute('/')({
59 component: () => {
60 // This component renders when the URL is exactly "/"
61 return (
62 <div>
63 <h2>Welcome!</h2>
64 <p>This is the public landing page.</p>
65 <p>Explore the navigation above.</p>
66 </div>
67 );
68 },
69});
70
71// src/routes/login.lazy.jsx (Public Login Page)
72// You already drafted the component logic in the previous response.
73// Use that component logic here. It needs useState, useNavigate, useAuth, api.
74import { createLazyFileRoute } from '@tanstack/react-router';
75// Import the actual Login component logic you create separately
76// import LoginPageContent from '../pages/LoginPageContent';
77
78export const Route = createLazyFileRoute('/login')({
79 component: () => (
80 // <LoginPageContent /> // Render your Login form component here
81 <div>Login Form Goes Here (See full repo)</div>
82 ),
83});
84
85// src/routes/register.lazy.jsx (Public Register Page)
86// Similar to login, use your Register component logic here.
87import { createLazyFileRoute } from '@tanstack/react-router';
88// import RegisterPageContent from '../pages/RegisterPageContent';
89
90export const Route = createLazyFileRoute('/register')({
91 component: () => (
92 // <RegisterPageContent /> // Render your Register form component here
93 <div>Register Form Goes Here (See full repo)</div>
94 ),
95});
96
97
98// src/routes/_auth.lazy.jsx (Layout Route for Authenticated Pages)
99// This route acts as a parent for all routes that require authentication.
100import { createLazyFileRoute, Outlet, redirect } from '@tanstack/react-router';
101import { useAuth } from '../AuthContext'; // Get the authentication state
102
103export const Route = createLazyFileRoute('/_auth')({
104 // The beforeLoad function runs BEFORE the route (or its children) are loaded.
105 beforeLoad: ({ location }) => {
106 // Access the auth state using your hook
107 const { user, loading } = useAuth();
108
109 // If still loading auth state, prevent loading immediately.
110 // TanStack Router can wait for promises, or you can handle this loading state in components.
111 // For this simple redirect example, we proceed only after loading is false.
112 if (loading) {
113 // If you wanted to show a global loading spinner, you might return a promise here
114 // that resolves when loading is done. For now, we'll just let it render and
115 // the component can show its own loading state if needed.
116 return; // Allow route to proceed while loading is true
117 }
118
119 // If the user is NOT authenticated (user is null) after loading is done
120 if (!user) {
121 console.log("User not authenticated, redirecting to login.");
122 // Throw a redirect response. TanStack Router catches this and navigates.
123 throw redirect({
124 to: '/login', // Redirect destination
125 // Optional: Pass the original location so the user can be redirected back after login
126 search: {
127 redirect: location.href,
128 },
129 });
130 }
131
132 // If authenticated (user is not null), allow the route and its children to load
133 console.log("User authenticated, allowing access.");
134 return;
135 },
136 // This component acts as a layout for all authenticated pages
137 component: () => (
138 <div>
139 <h3>Secured Area</h3>
140 {/* Optional: Add a sub-navigation menu for the secured area */}
141 <nav style={{ padding: '10px', borderBottom: '1px solid #ccc' }}>
142 <Link to="/dashboard">Dashboard</Link> |{' '}
143 <Link to="/secured">View Secured Data</Link>
144 </nav>
145 <div style={{ padding: '10px', border: '1px dashed green', margin: '10px 0' }}>
146 <Outlet /> {/* Render the matched child route (dashboard or secured) */}
147 </div>
148 </div>
149 ),
150});
151
152// src/routes/_auth/dashboard.lazy.jsx (Secured Dashboard Page)
153// This route is a child of the /_auth route, so it inherits the authentication check.
154import { createLazyFileRoute } from '@tanstack/react-router';
155import { useAuth } from '../../AuthContext'; // Get the authenticated user
156
157export const Route = createLazyFileRoute('/_auth/dashboard')({
158 component: () => {
159 const { user } = useAuth(); // Access the user object from context
160 return (
161 <div>
162 <h2>Dashboard</h2>
163 {user && <p>Welcome, {user.username}! You are logged in.</p>}
164 <p>This content is only visible to authenticated users.</p>
165 {/* Add dashboard-specific content here */}
166 </div>
167 );
168 },
169});
170
171// src/routes/_auth/secured.lazy.jsx (Another Secured Page)
172// Also a child of /_auth, automatically protected.
173import { createLazyFileRoute } from '@tanstack/react-router';
174import { useState, useEffect } from 'react';
175import api from '../../api'; // Your configured api client
176
177export const Route = createLazyFileRoute('/_auth/secured')({
178 component: () => {
179 const [data, setData] = useState(null);
180 const [error, setError] = useState('');
181 const [loading, setLoading] = useState(true);
182
183 useEffect(() => {
184 const fetchData = async () => {
185 try {
186 // Make a call to the secured backend endpoint
187 const response = await api.get('/secured');
188 setData(response.data);
189 setError('');
190 } catch (err) {
191 console.error('Error fetching secured data:', err);
192 // The response interceptor might handle 401, but catch others here
193 setError('Failed to fetch secured data.');
194 setData(null); // Clear previous data on error
195 } finally {
196 setLoading(false);
197 }
198 };
199 fetchData();
200 }, []); // Empty dependency array means run once on mount
201
202 return (
203 <div>
204 <h2>Secured Data Page</h2>
205 <p>This page fetches data from a protected API endpoint.</p>
206 {loading && <p>Loading data...</p>}
207 {error && <p style={{ color: 'red' }}>{error}</p>}
208 {data && <p>API Response: **{data}**</p>}
209 </div>
210 );
211 },
212});

Step 17: Generate the TanStack Router Tree

Open the VS Code terminal in your my-secure-app directory and run:

1npx @tanstack/router-cli generate

This command reads your src/routes files and generates src/routeTree.gen.ts (or .js). This file defines the structure for the router and provides type safety if you're using TypeScript. You shouldn't edit this file manually.

Step 18: Configure and Use the Router in App.jsx

Now, set up the main router instance in your src/App.jsx.

1// src/App.jsx
2import React from 'react';
3import { RouterProvider, createRouter } from '@tanstack/react-router';
4// Import the generated route tree
5import { routeTree } from './routeTree.gen';
6
7// Create a router instance
8const router = createRouter({ routeTree });
9
10// Register the router instance for type safety (optional but recommended for TypeScript)
11// You can remove this block if you are not using TypeScript
12declare module '@tanstack/react-router' {
13 interface Register {
14 router: typeof router;
15 }
16}
17
18function App() {
19 return (
20 // Provide the router instance to the application
21 <RouterProvider router={router} />
22 );
23}
24
25export default App;

Your React application is now set up with TanStack Router, JWT token handling via Axios, and protected routes using the beforeLoad hook and your authentication context!


Running the Complete Application

You need to run both the backend API and the React frontend simultaneously.

  1. Start the Backend API: Open a terminal in your MySecureApi folder and run:
    1dotnet run --urls="https://localhost:7100;http://localhost:5000" # Or your desired ports
    Your API should start and indicate the URLs it's listening on.
  2. Start the React Frontend: Open a different terminal in your my-secure-app folder and run:
    1npm run dev # If using Vite
    2# or
    3npm start # If using Create React App
    Your React app should open in your web browser (usually http://localhost:5173 for Vite or http://localhost:3000 for CRA).

Testing the Authentication Flow:

  1. Access the application in your browser. You should see the Home page.
  2. Try navigating to "Dashboard" or "Secured Data". You should be automatically redirected to the "Login" page by TanStack Router's beforeLoad hook.
  3. Go to the "Register" page and create a new user.
  4. Go to the "Login" page and log in with the user you just created. You should be redirected to the Dashboard.
  5. Now, try accessing the "Secured Data" page again. It should load successfully and display the data fetched from your backend's /api/secured endpoint.
  6. Click the "Logout" button. You should be logged out and redirected, and accessing secured pages should redirect you back to Login.
ASP.NET CoreReactJWT AuthSQLiteTanStack RouterFull Stack