Introduction
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.
Step 1: Setting Up the .NET Core 3.x 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.).
Step 2: Installing Dependencies
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
Step 3: Configuring JWT in Startup.cs
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!
Step 4: Generating JWT Tokens (Example Controller)
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.
Step 5: Building the Angular SPA
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.
Step 6: Creating the Auth Service in Angular
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.
Step 7: Using HTTP Interceptors to Attach JWT
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 { }
Step 8: Securing .NET Endpoints
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.
Step 9: Testing & Verifying Your Setup
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.
Best Practices & Gotchas
- 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.
Conclusion
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