Secure Web App with ASP.NET Core 8, SQL Server, and Angular 20

by Didin J. on Jul 14, 2025 Secure Web App with ASP.NET Core 8, SQL Server, and Angular 20

Build a secure web app using ASP.NET Core 8, SQL Server, and Angular 20 with JWT authentication, EF Core, and modern Angular standalone components.

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.

ASP Net Core, SQL Server, and Angular 7: Web App Authentication - Authentication Sequence Diagram

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

ASP Net Core, SQL Server, and Angular 7: Web App Authentication - Login Page
ASP Net Core, SQL Server, and Angular 7: Web App Authentication - Register Page
ASP Net Core, SQL Server, and Angular 7: Web App Authentication - List Page

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:

Thanks!