Getting Started with .NET Core 3.x, Angular SPA & JWT Auth

Getting Started with .NET Core 3.x, Angular SPA & JWT Auth

.NET Core
Angular
JWT
SPA
2021-03-19

Building modern web applications often involves a robust API backend and an interactive client-side frontend. In this post, we’ll explore how to create a Single-Page Application (SPA) using Angular that’s powered by a .NET Core 3.x backend. We’ll then secure this setup using JSON Web Tokens (JWT). By the end of this tutorial, you’ll have a functional understanding of how to implement token-based authentication in a .NET + Angular project.

To get started, let’s spin up a new .NET Core 3.x Web API. Open your terminal or command prompt, navigate to a folder where you want the new project to reside, and run:

dotnet new webapi -n DotNetCore3AngularJWT

This command creates a new folder named DotNetCore3AngularJWT containing a skeleton Web API project. Navigate into that folder and open the project in your preferred IDE (e.g., Visual Studio, VS Code, etc.).

To enable JWT-based authentication in .NET Core, we’ll install the Microsoft.AspNetCore.Authentication.JwtBearer package. From your IDE’s integrated terminal or the command line, run:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 3.1.0

With the necessary library installed, we can now configure authentication in Startup.cs. Locate the ConfigureServices method and add the following code:

public void ConfigureServices(IServiceCollection services) { services.AddControllers(); // Configuring JWT Authentication var key = Encoding.ASCII.GetBytes("YOUR_SECRET_KEY"); // Replace with a strong key services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(x => { x.RequireHttpsMetadata = false; x.SaveToken = true; x.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = false, ValidateAudience = false }; }); // Register application services, e.g. DB context, business logic, etc. } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); // Enable Authentication & Authorization app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }

Make sure to replace YOUR_SECRET_KEY with a secure, randomly generated string. Never commit real keys to a public repository!

To illustrate the token generation process, let’s create a new controller named AuthController. This controller will validate user credentials (mocked in this example) and return a JWT:

using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; namespace DotNetCore3AngularJWT.Controllers { [Route("api/[controller]")] [ApiController] public class AuthController : ControllerBase { [HttpPost("login")] public IActionResult Login([FromBody] UserLogin login) { // Mock user validation. Replace with your actual logic. if (login.Username == "test" && login.Password == "password") { var token = GenerateJwtToken(login.Username); return Ok(new { token }); } return Unauthorized(); } private string GenerateJwtToken(string username) { var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("YOUR_SECRET_KEY")); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims = new[] { new Claim(ClaimTypes.Name, username) }; var token = new JwtSecurityToken( issuer: null, audience: null, claims: claims, expires: DateTime.Now.AddHours(1), signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); } } public class UserLogin { public string Username { get; set; } public string Password { get; set; } } }

Here, we simply allow a user named test with password password. In a production environment, you’d connect this to a real database and handle password hashing, error handling, etc.

Next, let’s create our Angular application. In a separate directory from your .NET backend, run:

ng new angular-jwt-spa --routing=true --style=css

Navigate into the angular-jwt-spa folder, and you’ll see a default Angular structure generated by the CLI. We’ll set up a simple login flow and a protected route to demonstrate JWT authentication in action.

Let’s create a new service that handles the login request and token storage. Run:

ng generate service services/auth

In your newly created auth.service.ts file:

import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Router } from '@angular/router'; @Injectable({ providedIn: 'root' }) export class AuthService { private tokenKey = 'jwt_token'; constructor(private http: HttpClient, private router: Router) {} login(username: string, password: string) { return this.http.post<any>('https://localhost:5001/api/auth/login', { username, password }).subscribe(response => { if (response.token) { localStorage.setItem(this.tokenKey, response.token); this.router.navigate(['/protected']); } }); } logout() { localStorage.removeItem(this.tokenKey); this.router.navigate(['/login']); } getToken(): string | null { return localStorage.getItem(this.tokenKey); } isAuthenticated(): boolean { return !!this.getToken(); } }

We’re posting to our .NET Core API endpoint at /api/auth/login, storing the token in LocalStorage, and redirecting the user if successful.

To automatically include the JWT in every request, we can use Angular’s HTTP interceptors. Generate a new interceptor:

ng generate interceptor interceptors/jwt

Then in jwt.interceptor.ts:

import { Injectable } from '@angular/core'; import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { Observable } from 'rxjs'; import { AuthService } from '../services/auth.service'; @Injectable() export class JwtInterceptor implements HttpInterceptor { constructor(private authService: AuthService) {} intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const token = this.authService.getToken(); if (token) { const cloned = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); return next.handle(cloned); } return next.handle(req); } }

Finally, register this interceptor in your app.module.ts (or core.module.ts):

import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { JwtInterceptor } from './interceptors/jwt.interceptor'; // ... @NgModule({ declarations: [...], imports: [ BrowserModule, HttpClientModule, // ... ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true } ], bootstrap: [AppComponent] }) export class AppModule { }

To ensure that certain endpoints can only be called by authenticated users, decorate your controller methods with the [Authorize] attribute:

using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace DotNetCore3AngularJWT.Controllers { [Authorize] [Route("api/[controller]")] [ApiController] public class WeatherController : ControllerBase { [HttpGet] public IActionResult GetForecast() { // Return some protected resource return Ok(new { forecast = "Sunny with a chance of learning" }); } } }

Now, only authenticated requests bearing a valid JWT will successfully fetch this resource.

With everything in place, run your .NET Core backend:

dotnet run --project DotNetCore3AngularJWT

Then in a separate terminal, start your Angular dev server:

cd angular-jwt-spa ng serve --open

1. Go to the login page of your Angular app. 2. Enter valid credentials (as configured in AuthController — e.g., test /password). 3. You should be redirected to a protected route. Inspect the network requests in your browser dev tools to confirm that the JWT is being sent in the Authorization header.

  • Use HTTPS in Production: Always secure token transfers with TLS (HTTPS) to protect sensitive information.
  • Store Token Securely: While we used LocalStorage in this demo, consider a more secure method for token storage (like HttpOnly cookies) to mitigate XSS attacks.
  • Key Rotation & Token Expiry: Regularly rotate your signing keys and set reasonable token expirations to improve security.
  • Refresh Tokens When Needed: Implement a refresh token mechanism if your users need extended sessions without forcing them to log in repeatedly.
  • Check APIs: If you plan on supporting more complex scenarios, evaluate third-party libraries (like IdentityServer or Auth0) to streamline user management and token issuance.

Congratulations! You’ve built a basic end-to-end setup using .NET Core 3.x for your backend, Angular for your frontend SPA, and JWT for token-based authentication. We covered configuring the Startup.cs, generating JWT tokens in a custom controller, creating an Angular-based login flow, and securely passing tokens to protected endpoints.

This architecture offers a solid foundation for scaling your application with more advanced features like refresh tokens, role-based authorization, and external identity providers. If you have any questions or want to share how you extended this solution, feel free to reach out!

Happy coding!
– Nate