In this tutorial, we’ll build a secure full-stack web application using the latest technologies: ASP.NET Core 8, Angular 20, and SQL Server. We'll implement JWT (JSON Web Token) authentication, Entity Framework Core 8, and Angular standalone components. This is an updated and modernized version of the original tutorial that used ASP.NET Core 2 and Angular 7. We will create our own Microsoft SQL Server Database and Tables (User and Book). The password in the User table will be encrypted using salted HMACSHA512. The authentication flow is described in a sequence diagram below.
Prerequisites
-
.NET SDK 8.0
-
Node.js (LTS version)
-
Angular CLI 20
-
SQL Server 2019 or higher
docker pull mcr.microsoft.com/mssql/server:2022-latest docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=YourStrongPassword1" -p 1433:1433 --name sqlserver -d mcr.microsoft.com/mssql/server:2022-latest
-
Visual Studio Code or Visual Studio 2022+
-
Postman (optional for testing)
1. Create an ASP.NET Core 8 Web API Project
dotnet new webapi -n SecureWebApp
cd SecureWebApp
Install necessary NuGet packages:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 8.0.18
dotnet add package BCrypt.Net-Next
2. Set up Models and DB Context
Create Models/User.cs
:
namespace SecureWebApp.Models;
public class User
{
public int Id { get; set; }
public string FullName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public string PasswordHash { get; set; } = string.Empty;
}
Create Models/Book.cs
:
namespace SecureWebApp.Models;
public class Book
{
public int Id { get; set; }
public string Isbn { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Publisher { get; set; } = string.Empty;
public int? PublishedYear { get; set; } = 0;
public decimal? Price { get; set; } = 0;
}
Create Data/AppDbContext.cs
:
using Microsoft.EntityFrameworkCore;
namespace SecureWebApp.Models;
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<User> Users => Set<User>();
public DbSet<Book> Books => Set<Book>();
}
In appsettings.json, add:
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=ProductDb;Trusted_Connection=True;"
}
Update Program.cs
:
using Microsoft.EntityFrameworkCore;
using SecureWebApp.Models;
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
3. Configure JWT Authentication
In appsettings.json
:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Jwt": {
"Key": "YourSuperSecretKeyHere"
}
}
Update Program.cs
to add JWT services:
using Microsoft.EntityFrameworkCore;
using SecureWebApp.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
Add builder.Services.AddAuthorization();
before app.Run();
4. Create Auth Controller
Create DTOs/LoginDto.cs
:
namespace SecureWebApp.DTOs;
public class LoginDto
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
Create DTOs/RegisterDto.cs
:
namespace SecureWebApp.DTOs;
public class RegisterDto
{
public string FullName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
Create Controllers/AuthController.cs
:
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using SecureWebApp.DTOs;
using SecureWebApp.Models;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace SecureWebApp.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController(AppDbContext context, IConfiguration config) : ControllerBase
{
private readonly AppDbContext _context = context;
private readonly IConfiguration _config = config;
[HttpPost("register")]
public async Task<IActionResult> Register(RegisterDto registerDto)
{
var user = new User
{
FullName = registerDto.FullName,
Email = registerDto.Email,
Username = registerDto.Username,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(registerDto.Password)
};
_context.Users.Add(user);
await _context.SaveChangesAsync();
return Ok();
}
[HttpPost("login")]
public IActionResult Login(LoginDto userDto)
{
var user = _context.Users.SingleOrDefault(u => u.Username == userDto.Username);
if (user == null || !BCrypt.Net.BCrypt.Verify(userDto.Password, user.PasswordHash))
return Unauthorized();
var token = GenerateJwtToken(user);
return Ok(new { token });
}
private string GenerateJwtToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_config["Jwt:Key"]!);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, user.Username) }),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
Create Controllers/BookController.cs
:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SecureWebApp.Models;
namespace SecureWebApp.Controllers;
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class BookController(AppDbContext context) : ControllerBase
{
private readonly AppDbContext _context = context;
[HttpGet]
public async Task<IActionResult> GetBooks()
{
var data = await _context.Books.ToListAsync();
return Ok(data);
}
}
5. Set up Angular 20 Frontend
ng new secure-webapp-client --routing --style=css
cd secure-webapp-client
ng generate component login
ng generate component register
ng generate component book
ng generate service auth.service
ng generate service book.service
Update app.routes.ts
:
import { Routes } from '@angular/router';
import { Login } from './login/login';
import { Register } from './register/register';
import { Book } from './book/book';
export const routes: Routes = [
{ path: '', redirectTo: 'book', pathMatch: 'full' },
{ path: 'login', component: Login },
{ path: 'register', component: Register },
{ path: 'book', component: Book },
];
Update app.html
:
<div class="container">
<router-outlet />
</div>
Update app.css
:
.container {
padding: 20px;
}
6. HttpInterceptor (Angular)
Create src/app/token.interceptor.ts
:
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { catchError, map, Observable, throwError } from "rxjs";
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(private router: Router) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem('token');
if (token) {
request = request.clone({
setHeaders: {
'Authorization': 'Bearer ' + token
}
});
}
if (!request.headers.has('Content-Type')) {
request = request.clone({
setHeaders: {
'content-type': 'application/json'
}
});
}
request = request.clone({
headers: request.headers.set('Accept', 'application/json')
});
return next.handle(request).pipe(
map((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
console.log('event--->>>', event);
}
return event;
}),
catchError((error: HttpErrorResponse) => {
console.log(error);
if (error.status === 401) {
this.router.navigate(['login']);
}
if (error.status === 400) {
alert(error.error);
}
return throwError(() => error);
}));
}
}
Update src/app/app.config.ts
:
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { HTTP_INTERCEPTORS, provideHttpClient } from '@angular/common/http';
import { TokenInterceptor } from './token.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(),
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true
}
]
};
7. Auth and Book Service (Angular)
In src/app/auth.service.ts
:
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { tap } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private api = 'https://localhost:5104/api/auth';
constructor(private http: HttpClient, private router: Router) { }
login(data: any) {
return this.http.post(`${this.api}/login`, data).pipe(
tap((res: any) => {
localStorage.setItem('token', res.token);
this.router.navigate(['/dashboard']);
})
);
}
register(data: any) {
return this.http.post(`${this.api}/register`, data);
}
logout() {
localStorage.removeItem('token');
this.router.navigate(['/login']);
}
}
Create src/app/book.ts
:
export class Book {
bookId: number | undefined;
isbn: string | undefined;
title: string | undefined;
author: string | undefined;
description: string | undefined;
publisher: string | undefined;
publishedYear: number | undefined;
price: number | undefined;
}
Update src/app/book.service.ts
:
import { HttpClient } from '@angular/common/http';
import { Injectable, signal } from '@angular/core';
import { Router } from '@angular/router';
import { tap } from 'rxjs';
import { Book } from './book';
@Injectable({
providedIn: 'root'
})
export class BookService {
private api = 'https://localhost:5104/api/book';
private _books = signal<Book[]>([]);
constructor(private http: HttpClient, private router: Router) { }
getBooks() {
return this.http.get<Book[]>(this.api).pipe(
tap(data => this._books.set(data))
);
}
}
8. Create Login/Register Components
Install Angular Material.
ng add @angular/material
Update src/app/login/login.ts
:
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, FormsModule, NgForm, ReactiveFormsModule, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
import { Router } from '@angular/router';
import { AuthService } from '../auth.service';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatCardModule } from '@angular/material/card';
import { MatInputModule } from '@angular/material/input';
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
}
}
@Component({
selector: 'app-login',
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
MatCardModule,
MatInputModule,
],
templateUrl: './login.html',
styleUrl: './login.css'
})
export class Login {
loginForm: FormGroup;
email = '';
password = '';
matcher = new MyErrorStateMatcher();
isLoadingResults = false;
constructor(private formBuilder: FormBuilder, private router: Router, private authService: AuthService) {
this.loginForm = this.formBuilder.group({
'email': [null, Validators.required],
'password': [null, Validators.required]
});
}
onFormSubmit(form: NgForm) {
this.authService.login(form)
.subscribe({
next: (res) => {
console.log(res);
if (res.token) {
localStorage.setItem('token', res.token);
this.router.navigate(['book']);
}
}, error: (err) => {
console.log(err);
}
});
}
register() {
this.router.navigate(['register']);
}
}
Update src/app/login/login.html
:
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade" *ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<mat-card class="example-card">
<form [formGroup]="loginForm" (ngSubmit)="onFormSubmit(loginForm.value)">
<mat-form-field class="example-full-width">
<input
matInput
type="email"
placeholder="Email"
formControlName="email"
[errorStateMatcher]="matcher"
/>
<mat-error>
<span
*ngIf="
!loginForm.get('email')!.valid && loginForm.get('email')!.touched
"
>Please enter your email</span
>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input
matInput
type="password"
placeholder="Password"
formControlName="password"
[errorStateMatcher]="matcher"
/>
<mat-error>
<span
*ngIf="
!loginForm.get('password')!.valid &&
loginForm.get('password')!.touched
"
>Please enter your password</span
>
</mat-error>
</mat-form-field>
<div class="button-row">
<button
type="submit"
[disabled]="!loginForm.valid"
mat-flat-button
color="primary"
>
Login
</button>
</div>
<div class="button-row">
<button
type="button"
mat-flat-button
color="primary"
(click)="register()"
>
Register
</button>
</div>
</form>
</mat-card>
</div>
Update src/app/login/login.css
:
.example-container {
position: relative;
padding: 5px;
}
.example-form {
min-width: 150px;
max-width: 500px;
width: 100%;
}
.example-full-width {
width: 100%;
}
.example-full-width:nth-last-child() {
margin-bottom: 10px;
}
.button-row {
margin: 10px 0;
}
.mat-flat-button {
margin: 5px;
}
Update src/app/register/register.ts
:
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, FormsModule, NgForm, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { ErrorStateMatcher } from '@angular/material/core';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Router } from '@angular/router';
import { AuthService } from '../auth.service';
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
}
}
@Component({
selector: 'app-register',
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
MatCardModule,
MatInputModule,
],
templateUrl: './register.html',
styleUrl: './register.css'
})
export class Register {
registerForm: FormGroup;
fullName = '';
email = '';
password = '';
isLoadingResults = false;
matcher = new MyErrorStateMatcher();
constructor(private formBuilder: FormBuilder, private router: Router, private authService: AuthService) {
this.registerForm = this.formBuilder.group({
'fullName': [null, Validators.required],
'email': [null, Validators.required],
'password': [null, Validators.required]
});
}
onFormSubmit(form: NgForm) {
this.authService.register(form)
.subscribe({
next: () => {
this.router.navigate(['login']);
}, error: (err) => {
console.log(err);
alert(err.error);
}
});
}
}
Update src/app/register/register.html
:
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade" *ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<mat-card class="example-card">
<form
[formGroup]="registerForm"
(ngSubmit)="onFormSubmit(registerForm.value)"
>
<mat-form-field class="example-full-width">
<input
matInput
type="fullName"
placeholder="Full Name"
formControlName="fullName"
[errorStateMatcher]="matcher"
/>
<mat-error>
<span
*ngIf="
!registerForm.get('fullName')!.valid &&
registerForm.get('fullName')!.touched
"
>Please enter your Full Name</span
>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input
matInput
type="email"
placeholder="Email"
formControlName="email"
[errorStateMatcher]="matcher"
/>
<mat-error>
<span
*ngIf="
!registerForm.get('email')!.valid &&
registerForm.get('email')!.touched
"
>Please enter your email</span
>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input
matInput
type="password"
placeholder="Password"
formControlName="password"
[errorStateMatcher]="matcher"
/>
<mat-error>
<span
*ngIf="
!registerForm.get('password')!.valid &&
registerForm.get('password')!.touched
"
>Please enter your password</span
>
</mat-error>
</mat-form-field>
<div class="button-row">
<button
type="submit"
[disabled]="!registerForm.valid"
mat-flat-button
color="primary"
>
Register
</button>
</div>
</form>
</mat-card>
</div>
Update src/app/register/register.css
:
.example-container {
position: relative;
padding: 5px;
}
.example-form {
min-width: 150px;
max-width: 500px;
width: 100%;
}
.example-full-width {
width: 100%;
}
.example-full-width:nth-last-child() {
margin-bottom: 10px;
}
.button-row {
margin: 10px 0;
}
.mat-flat-button {
margin: 5px;
}
9. Create Secure Book List
Update src/app/book/book.ts
:
import { CommonModule } from '@angular/common';
import { Component, signal } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BookService } from '../book.service';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatCardModule } from '@angular/material/card';
import { MatTableModule } from '@angular/material/table';
@Component({
selector: 'app-book',
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
MatCardModule,
MatTableModule
],
templateUrl: './book.html',
styleUrl: './book.css'
})
export class Book {
get books() {
return this.bookService.getBooks();
}
displayedColumns: string[] = ['bookId', 'isbn', 'title'];
isLoadingResults = true;
constructor(private bookService: BookService, private authService: AuthService, private router: Router) { }
logout() {
localStorage.removeItem('token');
this.router.navigate(['login']);
}
}
Update src/app/book/book.html
:
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade" *ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" (click)="logout()">Logout</a>
</div>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="books" class="example-table">
<!-- Book ID Column -->
<ng-container matColumnDef="bookId">
<th mat-header-cell *matHeaderCellDef>Book ID</th>
<td mat-cell *matCellDef="let row">{{ row.bookId }}</td>
</ng-container>
<!-- ISBN Column -->
<ng-container matColumnDef="isbn">
<th mat-header-cell *matHeaderCellDef>ISBN</th>
<td mat-cell *matCellDef="let row">{{ row.isbn }}</td>
</ng-container>
<!-- Title Column -->
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef>Title</th>
<td mat-cell *matCellDef="let row">{{ row.title }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
</div>
Update src/app/book/book.css
:
.example-container {
position: relative;
padding: 5px;
}
.example-table-container {
position: relative;
max-height: 400px;
overflow: auto;
}
table {
width: 100%;
}
.example-loading-shade {
position: absolute;
top: 0;
left: 0;
bottom: 56px;
right: 0;
background: rgba(0, 0, 0, 0.15);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
.example-rate-limit-reached {
color: #980000;
max-width: 360px;
text-align: center;
}
/* Column Widths */
.mat-column-number,
.mat-column-state {
max-width: 64px;
}
.mat-column-created {
max-width: 124px;
}
.mat-flat-button {
margin: 5px;
}
10. Run the App
Run EF migrations and backend:
dotnet ef migrations add Init
dotnet ef database update
dotnet run
Run Angular:
cd secure-webapp-client
ng serve
Conclusion
You’ve just built a modern full-stack secure web app with:
-
ASP.NET Core 8 for backend API
-
Angular 20 with standalone components
-
SQL Server and EF Core 8 for data
-
JWT Authentication for secure access
This setup ensures performance, maintainability, and scalability for production-grade applications.
You can find the full source code on our GitHub.
That's just the basics. If you need more deep learning about ASP.NET Core, Angular, or related, you can take the following cheap course:
- ANGULAR and ASP. NET Core REST API - Real World Application
- Creating GraphQL APIs with ASP. Net Core for Beginners
- ASP .Net MVC Quick Start
- Master SignalR: Build Real-Time Web Apps with ASP. NET
- Fullstack Asp. Net Core MVC & C# Bootcamp With Real Projects
- ASP. NET Core MVC - A Step by Step Course
Thanks!