How Do You Secure an Angular + ASP.NET Core Application?
1. What is Security in Angular + ASP.NET Core?
In an Angular + ASP.NET Core application, security means protecting:
- Authentication — Who is the user?
- Authorization — What is the user allowed to do?
- API endpoints
- Sensitive data
- User credentials
- Tokens/cookies
- Database access
- Communication between browser and server
- Application configuration and secrets
The most important architectural principle is:
Angular can improve the user experience around security, but ASP.NET Core must enforce the actual security rules.
For example, an Angular route guard can prevent a normal user from navigating to /admin, but it cannot securely protect the underlying /api/admin endpoint. The ASP.NET Core API must independently authorize the request.
2. Typical Secure Architecture
HTTPS
|
v
+-----------------------------+
| Angular Browser |
| |
| Login Form |
| Route Guards |
| HTTP Interceptor |
| Input Validation |
+-------------+---------------+
|
| HTTPS
| Authorization: Bearer <JWT>
v
+-----------------------------------------+
| ASP.NET Core Web API |
| |
| Authentication |
| Authorization |
| JWT Validation |
| Role / Policy Authorization |
| CORS |
| Rate Limiting |
| Input Validation |
| Exception Handling |
+--------------------+--------------------+
|
v
+-----------------------------------------+
| SQL Server |
| |
| Parameterized Queries / EF Core |
| Least-privilege DB account |
+-----------------------------------------+
Secrets:
Azure Key Vault / Environment Variables
A production application normally uses multiple security layers, rather than relying on one mechanism such as JWT.
3. Full Simple Sample Program
Consider an Angular application calling:
https://localhost:7001/api/products
We will protect the endpoint using JWT.
ASP.NET Core Web API
appsettings.json
{
"Jwt": {
"Issuer": "MyApi",
"Audience": "MyAngularApp",
"Key": "ThisIsOnlyADevelopmentExampleSecretKey123456789"
}
}
Important: this is for demonstration only. A production signing secret should not be committed to appsettings.json or source control.
Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var jwtKey = builder.Configuration["Jwt:Key"]!;
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters =
new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey =
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtKey))
};
});
builder.Services.AddAuthorization();
builder.Services.AddCors(options =>
{
options.AddPolicy("AngularApp", policy =>
{
policy
.WithOrigins("http://localhost:4200")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", limiter =>
{
limiter.PermitLimit = 100;
limiter.Window = TimeSpan.FromMinutes(1);
});
});
var app = builder.Build();
app.UseHttpsRedirection();
app.UseCors("AngularApp");
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
AuthController.cs
For simplicity, the username and password are hard-coded.
In a real application, the user would normally be retrieved from a database and the password verified using a secure password hashing mechanism or an identity provider.
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IConfiguration _configuration;
public AuthController(IConfiguration configuration)
{
_configuration = configuration;
}
[HttpPost("login")]
public IActionResult Login(LoginRequest request)
{
// Demo only
if (request.Username != "admin" ||
request.Password != "Admin@123")
{
return Unauthorized();
}
var claims = new[]
{
new Claim(ClaimTypes.Name, request.Username),
new Claim(ClaimTypes.Role, "Admin")
};
var key =
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(
_configuration["Jwt:Key"]!));
var credentials =
new SigningCredentials(
key,
SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(15),
signingCredentials: credentials);
var jwt =
new JwtSecurityTokenHandler()
.WriteToken(token);
return Ok(new
{
accessToken = jwt
});
}
}
public record LoginRequest(
string Username,
string Password);
ProductsController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ProductsController : ControllerBase
{
[HttpGet]
public IActionResult GetProducts()
{
var products = new[]
{
new
{
Id = 1,
Name = "Laptop",
Price = 50000
},
new
{
Id = 2,
Name = "Mouse",
Price = 1000
}
};
return Ok(products);
}
[HttpDelete("{id}")]
[Authorize(Roles = "Admin")]
public IActionResult DeleteProduct(int id)
{
return Ok(new
{
Message = $"Product {id} deleted"
});
}
}
Now:
GET /api/products
requires an authenticated user.
And:
DELETE /api/products/1
requires:
Role = Admin
Angular Application
The Angular side needs to:
- Login
- Receive the access token
- Attach it to API requests
- Handle
401/403 - Use guards for navigation UX
auth.service.ts
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs';
interface LoginResponse {
accessToken: string;
}
@Injectable({
providedIn: 'root'
})
export class AuthService {
private http = inject(HttpClient);
private apiUrl =
'https://localhost:7001/api/auth';
login(username: string, password: string) {
return this.http
.post<LoginResponse>(
`${this.apiUrl}/login`,
{
username,
password
}
)
.pipe(
tap(response => {
// Simplified demo storage.
sessionStorage.setItem(
'access_token',
response.accessToken
);
})
);
}
getToken(): string | null {
return sessionStorage.getItem(
'access_token'
);
}
isLoggedIn(): boolean {
return !!this.getToken();
}
logout(): void {
sessionStorage.removeItem(
'access_token'
);
}
}
For this simple demonstration we're using sessionStorage. Token storage deserves its own security decision; for many production browser applications, an architecture using secure HttpOnly cookies/BFF or carefully designed short-lived access tokens can reduce exposure to token theft.
auth.interceptor.ts
import {
HttpInterceptorFn
} from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService }
from './auth.service';
export const authInterceptor:
HttpInterceptorFn = (req, next) => {
const authService =
inject(AuthService);
const token =
authService.getToken();
if (!token) {
return next(req);
}
const authRequest =
req.clone({
setHeaders: {
Authorization:
`Bearer ${token}`
}
});
return next(authRequest);
};
Every outgoing API request can now contain:
Authorization: Bearer eyJhbGciOi...
error.interceptor.ts
import {
HttpErrorResponse,
HttpInterceptorFn
} from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';
export const errorInterceptor:
HttpInterceptorFn = (req, next) => {
const router = inject(Router);
return next(req).pipe(
catchError(
(error: HttpErrorResponse) => {
if (error.status === 401) {
router.navigate(['/login']);
}
else if (error.status === 403) {
router.navigate(['/unauthorized']);
}
return throwError(
() => error
);
}
)
);
};
This gives us centralized handling for common authentication/authorization failures.
auth.guard.ts
import { inject } from '@angular/core';
import {
CanActivateFn,
Router
} from '@angular/router';
import { AuthService }
from './auth.service';
export const authGuard:
CanActivateFn = () => {
const authService =
inject(AuthService);
const router =
inject(Router);
if (authService.isLoggedIn()) {
return true;
}
return router.createUrlTree(
['/login']
);
};
app.routes.ts
import { Routes } from '@angular/router';
import { authGuard }
from './auth.guard';
import { Login }
from './login';
import { Products }
from './products';
import { Unauthorized }
from './unauthorized';
export const routes: Routes = [
{
path: 'login',
component: Login
},
{
path: 'products',
component: Products,
canActivate: [authGuard]
},
{
path: 'unauthorized',
component: Unauthorized
},
{
path: '',
redirectTo: 'products',
pathMatch: 'full'
}
];
app.config.ts
import {
ApplicationConfig
} from '@angular/core';
import {
provideRouter
} from '@angular/router';
import {
provideHttpClient,
withInterceptors
} from '@angular/common/http';
import { routes }
from './app.routes';
import { authInterceptor }
from './auth.interceptor';
import { errorInterceptor }
from './error.interceptor';
export const appConfig:
ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(
withInterceptors([
authInterceptor,
errorInterceptor
])
)
]
};
This is the modern standalone Angular configuration.
4. How the Security Flow Works
Suppose the user opens:
/products
The flow is:
User
|
v
Angular Login
|
| username/password
v
POST /api/auth/login
|
v
ASP.NET Core
|
| Validate credentials
| Generate JWT
|
v
Angular
|
| Store token
|
v
GET /api/products
|
| Auth Interceptor
| adds:
|
| Authorization: Bearer <token>
|
v
ASP.NET Core
|
| Authentication Middleware
| validates JWT
|
v
Authorization
|
| [Authorize]
|
v
ProductsController
|
v
200 OK
If the token is invalid or expired:
API
|
v
401 Unauthorized
|
v
Angular Error Interceptor
|
v
/login
If the user is authenticated but doesn't have the required permission:
API
|
v
403 Forbidden
|
v
Angular Error Interceptor
|
v
/unauthorized
5. Authentication vs Authorization
These are frequently confused in interviews.
Authentication
Authentication answers:
Who are you?
Example:
[Authorize]
The API requires an authenticated user.
Authorization
Authorization answers:
What are you allowed to do?
Example:
[Authorize(Roles = "Admin")]
Only users with the Admin role can access the endpoint.
For more complex applications, policy-based authorization is generally preferable to spreading role checks everywhere.
6. Angular Route Guards Are NOT Security
Suppose Angular has:
{
path: 'admin',
canActivate: [adminGuard],
component: Admin
}
The guard can prevent normal navigation to:
/admin
But an attacker doesn't need to use your Angular UI.
They can call:
DELETE https://api.example.com/api/products/10
directly using another HTTP client.
Therefore this:
canActivate: [adminGuard]
is not sufficient.
The API must enforce authorization:
[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
...
}
Think of it this way:
Angular Guard
|
| UX/navigation protection
v
Angular Page
ASP.NET Core [Authorize]
|
| Actual security boundary
v
Protected resource
7. Use HTTPS
Production communication should use:
HTTPS
rather than:
HTTP
HTTPS protects data in transit using TLS.
For example:
Angular
|
| HTTPS
| encrypted
v
ASP.NET Core API
ASP.NET Core commonly includes:
app.UseHttpsRedirection();
Production deployments should also have appropriate TLS and HSTS configuration.
8. Configure CORS Correctly
Avoid unnecessarily broad production policies.
For example, don't default to:
policy.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
Instead specify the Angular origin:
policy
.WithOrigins(
"https://www.example.com")
.AllowAnyHeader()
.AllowAnyMethod();
Also remember:
CORS is a browser cross-origin policy, not an authentication or authorization mechanism.
It does not replace [Authorize].
9. Never Store Secrets in Angular
This is extremely important.
Don't put secrets such as these in Angular:
export const environment = {
databasePassword: 'password123',
jwtSigningKey:
'MySecretSigningKey',
stripeSecret:
'secret-value'
};
Angular runs in the user's browser.
Anything delivered to the browser should effectively be considered accessible to the user.
Instead:
Angular
|
| Normal API request
v
ASP.NET Core
|
| Reads secret
v
Environment Variable
or
Azure Key Vault
Sensitive secrets belong on the trusted server side.
10. Prevent SQL Injection
Never construct SQL using raw user input like:
var sql =
"SELECT * FROM Users WHERE Name = '"
+ username +
"'";
Use parameterized queries.
EF Core normally parameterizes values in LINQ queries:
var user =
await db.Users
.FirstOrDefaultAsync(
x => x.Username == username);
For raw SQL, use parameterized APIs rather than string concatenation.
11. Prevent XSS
Cross-Site Scripting occurs when attacker-controlled content is executed as JavaScript in another user's browser.
Angular automatically sanitizes/escapes many values in normal template usage.
For example:
<div>
{{ comment }}
</div>
is much safer than deliberately bypassing Angular's sanitization.
Be particularly careful with:
[innerHTML]
and especially with APIs such as DomSanitizer.bypassSecurityTrustHtml().
Never treat arbitrary user HTML as trusted simply to make it render.
12. CSRF Protection
CSRF is particularly important when authentication credentials are automatically sent by the browser, such as cookies.
For cookie-based authentication, use appropriate anti-forgery protection, cookie settings, and same-site strategy.
For example, cookies containing authentication credentials should generally be configured with security attributes such as:
Secure
HttpOnly
SameSite
The exact SameSite value depends on whether your Angular application and API are same-site or cross-site.
13. Password Security
Never store:
Password = "Admin@123"
directly in a database.
Store a secure password hash using a proven password-hashing implementation.
Common ASP.NET Core applications use:
ASP.NET Core Identity
or an external identity provider rather than implementing password storage themselves.
Conceptually:
Password
|
v
Password Hashing
|
v
Hash
|
v
Database
14. Rate Limiting
Rate limiting helps protect endpoints from excessive traffic and some abuse patterns.
For example:
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter(
"fixed",
limiter =>
{
limiter.PermitLimit = 100;
limiter.Window =
TimeSpan.FromMinutes(1);
});
});
It is particularly useful around endpoints such as:
/login
/password-reset
/otp
/search
/public APIs
Authentication endpoints often need stricter, identity/IP-aware anti-abuse controls than a generic global request limit.
15. Validate Input on Both Sides
Angular validation:
Validators.required
Validators.email
Validators.minLength(8)
is useful for UX.
But users can bypass Angular entirely.
Therefore ASP.NET Core must validate the request independently.
For example:
public class RegisterRequest
{
[Required]
[EmailAddress]
public string Email { get; set; } = "";
[Required]
[MinLength(8)]
public string Password { get; set; } = "";
}
The rule is:
Angular validation
+
Server validation
But only the server-side validation is part of the security boundary.
16. Don't Expose Internal Exception Details
Don't send detailed production exceptions such as:
SqlException:
Login failed for user...
Server=...
Database=...
StackTrace...
to the browser.
Instead return a controlled error format such as ASP.NET Core ProblemDetails:
{
"type": "https://example.com/errors/server-error",
"title": "An unexpected error occurred.",
"status": 500
}
Keep detailed diagnostic information in protected server logs.
17. Access Token + Refresh Token
A stronger JWT design commonly uses:
Access Token
+
Refresh Token
For example:
Access Token
Lifetime: short
Purpose: API access
Refresh Token
Lifetime: longer
Purpose: obtain a new access token
Flow:
Login
|
v
Access Token + Refresh Token
|
v
Angular calls API
|
v
Access token expires
|
v
Refresh endpoint
|
v
New access token
Refresh tokens should be protected carefully and support rotation/revocation where appropriate.
18. Security Responsibilities
A useful way to remember the architecture is:
| Security concern | Angular | ASP.NET Core |
|---|---|---|
| Login UI | Yes | Validates credentials |
| Route guard | Yes | No |
| JWT attachment | Yes | Validates JWT |
| Authentication enforcement | No | Yes |
| Authorization enforcement | No | Yes |
| Form validation | Yes | Yes |
| CORS | No | Yes |
| Rate limiting | No | Yes |
| Database protection | No | Yes |
| Secret storage | Never | Yes |
| XSS mitigation | Yes | Also validate/encode appropriately |
| CSRF protection | Depends on auth model | Yes |
| Logging/auditing | Limited | Yes |
19. Production Security Checklist
For an Angular + ASP.NET Core application, remember these core controls:
- HTTPS/TLS everywhere
- Strong authentication
- Server-side authorization
- Short-lived access tokens or secure cookie-based sessions
- Secure refresh/session lifecycle
- Angular HTTP interceptor
- Angular route guards for UX only
- Correct CORS allowlist
- Server-side request validation
- XSS prevention
- CSRF protection when applicable
- Parameterized database access
- Rate limiting / anti-abuse controls
- Secrets outside Angular/source control
- Secure password hashing / established identity provider
- Safe error responses
- Security logging and auditing
- Least-privilege access to databases and infrastructure
- Security headers and secure cookie configuration
- Keep Angular, .NET, npm and NuGet dependencies patched
20. Key Points
Point 1 — Backend is the security boundary
Never depend on Angular alone.
Angular = client
ASP.NET Core = trusted security enforcement
Point 2 — Guards aren't authorization
canActivate: [authGuard]
improves navigation behavior.
Actual protection belongs in ASP.NET Core:
[Authorize]
or:
[Authorize(Policy = "CanDeleteProducts")]
Point 3 — Never put secrets in Angular
Anything shipped to Angular can be inspected by the user.
Secret → Backend only
Point 4 — 401 and 403 are different
401 Unauthorized
↓
Not authenticated / authentication failed
403 Forbidden
↓
Authenticated but not permitted
Despite the HTTP name 401 Unauthorized, it represents an authentication problem.
Point 5 — Use defense in depth
Don't think:
JWT = application is secure
Think:
HTTPS
+
Authentication
+
Authorization
+
Validation
+
XSS/CSRF defenses
+
CORS
+
Rate limiting
+
Secure secrets
+
Database security
+
Logging
+
Dependency patching
Interview Questions and Answers
1. How do you secure an Angular + ASP.NET Core application?
Use HTTPS, strong authentication, server-side authorization, secure token/cookie handling, input validation, restricted CORS, XSS/CSRF protection, rate limiting, safe database access, secure secret storage, logging, and least-privilege access.
The ASP.NET Core backend must enforce security because Angular runs on the client and can be bypassed.
2. Is an Angular route guard sufficient for security?
No.
A route guard only controls Angular navigation. Users can bypass Angular and directly call the API, so ASP.NET Core must enforce authorization using [Authorize], roles, policies, or resource-based authorization.
3. Where should JWT validation happen?
In ASP.NET Core.
.AddAuthentication()
.AddJwtBearer(...)
Angular may carry and attach the token, but the API validates its signature, issuer, audience, lifetime, and claims.
4. Where should secrets be stored?
Server-side, using mechanisms such as:
Environment variables
Azure Key Vault
Secret managers
Protected configuration
Never store database passwords, JWT signing secrets, or private API credentials in Angular.
5. What is the difference between 401 and 403?
401 means authentication is missing or failed.
403 means the user is authenticated but doesn't have permission to perform the requested operation.
6. Is CORS a security/authentication mechanism?
No.
CORS controls which browser origins are permitted to make cross-origin requests. It does not replace authentication or authorization.
7. Should validation be done in Angular or ASP.NET Core?
Both, but for different reasons.
Angular validation provides immediate user feedback. ASP.NET Core validation is mandatory because client-side validation can be bypassed.
8. How do you prevent SQL injection?
Use EF Core parameterized LINQ queries or explicit parameterized SQL. Never concatenate untrusted input into SQL statements.
9. How do you prevent XSS in Angular?
Use Angular's normal template binding and sanitization, avoid unsafe DOM manipulation, don't blindly render untrusted HTML, and avoid bypassing Angular sanitization unless the content is genuinely trusted.
10. Where should authorization be implemented?
Authorization must be enforced in ASP.NET Core.
For example:
[Authorize(Roles = "Admin")]
or preferably for more complex requirements:
[Authorize(Policy = "CanDeleteProducts")]
Angular can mirror those permissions to hide buttons or protect routes, but that is UX rather than the security boundary.
Interview Quick Answer
How do you secure Angular + ASP.NET Core?
Answer:
I secure the application using HTTPS, authentication with JWT or secure cookies, server-side role/policy authorization, Angular interceptors and route guards, strict CORS configuration, server-side validation, XSS and CSRF protection, rate limiting, parameterized database access, secure secret storage such as Azure Key Vault, safe exception handling, logging, and least-privilege access. Angular guards improve UX, but the ASP.NET Core API always performs the real authentication and authorization checks.