JWT Authentication Between Angular and ASP.NET Core Web API
1. What is JWT Authentication?
JWT (JSON Web Token) authentication is a token-based authentication mechanism commonly used between an Angular SPA and an ASP.NET Core Web API.
The basic idea is:
Angular sends username/password → Web API validates them → API creates a JWT → Angular sends that JWT with later requests → Web API validates the JWT before allowing access.
JWT stands for JSON Web Token.
A JWT typically contains:
- User identifier
- Username/email
- Roles or other claims
- Expiration time
- Issuer
- Audience
- Digital signature
A JWT has three parts:
Header.Payload.Signature
Example:
xxxxx.yyyyy.zzzzz
2. Overall Authentication Flow
LOGIN
Angular ASP.NET Core Web API
| |
| POST /api/auth/login |
| username + password ---------------->|
| |
| Validate credentials
| |
| Generate JWT
| |
|<-------------------------- JWT Token |
| |
| Store/use token |
| |
| |
| GET /api/products |
| Authorization: Bearer JWT ---------->|
| |
| Validate JWT
| Validate signature
| Validate expiry
| Validate issuer
| Validate audience
| |
|<------------------------- 200 + Data |
The important point is that the password is sent only during login. After successful login, the JWT is used for authenticated API requests.
3. Full Simple Sample Program
We'll create:
Angular
|
| POST /api/auth/login
↓
ASP.NET Core Web API
|
| returns JWT
↓
Angular
|
| Authorization: Bearer <token>
↓
GET /api/products
|
↓
[Authorize]
Part 1 — ASP.NET Core Web API
Install Package
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
appsettings.json
{
"Jwt": {
"Key": "ThisIsMySuperSecretKeyForJwtAuthentication12345",
"Issuer": "MyWebApi",
"Audience": "MyAngularApp"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
In a real application, don't keep production signing secrets directly in
appsettings.json. Use a secure secret store or environment-specific secret mechanism.
Models/LoginRequest.cs
namespace JwtDemo.Models;
public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
Models/LoginResponse.cs
namespace JwtDemo.Models;
public class LoginResponse
{
public string Token { get; set; } = string.Empty;
}
Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// -----------------------------------------
// CORS
// -----------------------------------------
builder.Services.AddCors(options =>
{
options.AddPolicy("AngularApp", policy =>
{
policy
.WithOrigins("http://localhost:4200")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
// -----------------------------------------
// JWT Authentication
// -----------------------------------------
var jwtKey = builder.Configuration["Jwt:Key"]!;
var jwtIssuer = builder.Configuration["Jwt:Issuer"]!;
var jwtAudience = builder.Configuration["Jwt:Audience"]!;
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters =
new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuer,
ValidAudience = jwtAudience,
IssuerSigningKey =
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtKey))
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseCors("AngularApp");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Notice the order:
app.UseAuthentication();
app.UseAuthorization();
Authentication identifies the user first; authorization then determines whether that user can access the endpoint.
Controllers/AuthController.cs
using JwtDemo.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace JwtDemo.Controllers;
[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.
// Normally validate against database
// and verify a securely hashed password.
if (request.Username != "admin" ||
request.Password != "admin123")
{
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(30),
signingCredentials:
credentials);
var jwt =
new JwtSecurityTokenHandler()
.WriteToken(token);
return Ok(new LoginResponse
{
Token = jwt
});
}
}
Controllers/ProductsController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace JwtDemo.Controllers;
[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);
}
}
Because the controller has:
[Authorize]
the API requires a valid JWT.
Part 2 — Angular
Assume a modern standalone Angular application.
app.config.ts
import { ApplicationConfig } from '@angular/core';
import {
provideHttpClient,
withInterceptors
} from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([
authInterceptor
])
)
]
};
auth.service.ts
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, tap } from 'rxjs';
interface LoginResponse {
token: string;
}
@Injectable({
providedIn: 'root'
})
export class AuthService {
private http = inject(HttpClient);
private apiUrl =
'https://localhost:7001/api/auth';
login(
username: string,
password: string
): Observable<LoginResponse> {
return this.http
.post<LoginResponse>(
`${this.apiUrl}/login`,
{
username,
password
}
)
.pipe(
tap(response => {
localStorage.setItem(
'token',
response.token
);
})
);
}
getToken(): string | null {
return localStorage.getItem('token');
}
logout(): void {
localStorage.removeItem('token');
}
}
For simplicity this demo uses localStorage. In production, token storage is a security design decision: localStorage is accessible to JavaScript and therefore exposed if the application has an XSS vulnerability. Cookie-based approaches can reduce token exposure to JavaScript when configured correctly.
auth.interceptor.ts
import {
HttpInterceptorFn
} from '@angular/common/http';
export const authInterceptor:
HttpInterceptorFn = (req, next) => {
const token =
localStorage.getItem('token');
if (token) {
const clonedRequest =
req.clone({
setHeaders: {
Authorization:
`Bearer ${token}`
}
});
return next(clonedRequest);
}
return next(req);
};
The interceptor automatically adds:
Authorization: Bearer eyJhbGciOi...
to API requests.
product.service.ts
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class ProductService {
private http = inject(HttpClient);
private apiUrl =
'https://localhost:7001/api/products';
getProducts() {
return this.http.get(this.apiUrl);
}
}
Notice that ProductService does not manually add the JWT.
We simply write:
this.http.get(this.apiUrl);
The interceptor handles it.
login.ts
import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AuthService } from './auth.service';
@Component({
selector: 'app-login',
standalone: true,
imports: [
FormsModule
],
template: `
<h2>Login</h2>
<input
[(ngModel)]="username"
placeholder="Username">
<br><br>
<input
[(ngModel)]="password"
type="password"
placeholder="Password">
<br><br>
<button (click)="login()">
Login
</button>
`
})
export class Login {
private authService =
inject(AuthService);
username = '';
password = '';
login() {
this.authService
.login(
this.username,
this.password
)
.subscribe({
next: response => {
console.log(
'Login successful',
response
);
},
error: error => {
console.log(
'Login failed',
error
);
}
});
}
}
products.ts
import { Component, inject } from '@angular/core';
import { ProductService } from './product.service';
@Component({
selector: 'app-products',
standalone: true,
template: `
<h2>Products</h2>
<button (click)="loadProducts()">
Load Products
</button>
<pre>
{{ products | json }}
</pre>
`
})
export class Products {
private productService =
inject(ProductService);
products: any;
loadProducts() {
this.productService
.getProducts()
.subscribe({
next: data => {
this.products = data;
},
error: error => {
console.log(error);
}
});
}
}
For | json, import JsonPipe into the standalone component if you use this exact template:
import { JsonPipe } from '@angular/common';
and:
imports: [JsonPipe]
4. How the Complete Flow Works
Let's understand the program step by step.
Step 1 — User enters credentials
Angular collects:
Username: admin
Password: admin123
Angular calls:
POST /api/auth/login
with:
{
"username": "admin",
"password": "admin123"
}
Step 2 — ASP.NET Core validates credentials
The demo checks:
if (request.Username != "admin" ||
request.Password != "admin123")
{
return Unauthorized();
}
In a real application this would normally involve:
Angular
↓
AuthController
↓
User Service
↓
Database
↓
Password hash verification
You should never store plaintext passwords in a production database.
Step 3 — Web API creates claims
The API creates claims such as:
new Claim(
ClaimTypes.Name,
request.Username)
new Claim(
ClaimTypes.Role,
"Admin")
Claims are information about the authenticated identity.
For example:
Name = admin
Role = Admin
UserId = 1001
Step 4 — Web API creates JWT
The API generates the token:
var token =
new JwtSecurityToken(
issuer: "MyWebApi",
audience: "MyAngularApp",
claims: claims,
expires: DateTime.UtcNow.AddMinutes(30),
signingCredentials: credentials
);
The API signs it using the configured signing key.
Conceptually:
Header
+
Payload
+
Signing Key
↓
Signature
Result:
xxxxx.yyyyy.zzzzz
5. API Returns JWT to Angular
The API returns:
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}
Angular receives it:
tap(response => {
localStorage.setItem(
'token',
response.token
);
})
So the browser now has the access token available to the Angular application.
6. Angular Calls a Protected API
Suppose Angular executes:
this.http.get(
'https://localhost:7001/api/products'
);
The products endpoint contains:
[Authorize]
Therefore, it requires authentication.
7. Interceptor Adds JWT
Instead of adding the token in every service:
headers: {
Authorization: `Bearer ${token}`
}
we use an interceptor.
The interceptor automatically changes the request from:
GET /api/products
to:
GET /api/products
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
This is one of the main reasons authentication interceptors are useful.
8. ASP.NET Core Extracts the JWT
The JWT Bearer authentication handler examines:
Authorization: Bearer <JWT>
The configuration comes from:
.AddJwtBearer(options =>
{
options.TokenValidationParameters =
new TokenValidationParameters
{
...
};
});
ASP.NET Core validates the token before the protected controller action executes.
9. What Does ASP.NET Core Validate?
The API can validate several important properties.
Signature
ValidateIssuerSigningKey = true
Checks that the token was signed using a trusted signing key.
Expiration
ValidateLifetime = true
If the token is expired:
Token expired
↓
Authentication fails
↓
401 Unauthorized
Issuer
ValidateIssuer = true
Checks who issued the token.
Example:
MyWebApi
Audience
ValidateAudience = true
Checks that the token is intended for the configured audience.
Example:
MyAngularApp
10. [Authorize] Protects the Endpoint
Consider:
[Authorize]
[HttpGet]
public IActionResult GetProducts()
{
...
}
Valid JWT:
JWT
↓
Authentication succeeds
↓
[Authorize] succeeds
↓
Controller executes
↓
200 OK
Missing, invalid, or expired JWT:
No valid authentication
↓
[Authorize] fails
↓
401 Unauthorized
11. Authentication vs Authorization
These are closely related but different.
Authentication answers:
Who are you?
Example:
User = admin
Authorization answers:
What are you allowed to do?
Example:
Role = Admin
You could protect an endpoint with:
[Authorize(Roles = "Admin")]
Then only authenticated users with the Admin role claim can access it.
12. JWT Authentication Architecture
┌──────────────────────┐
│ Angular │
│ │
│ Login Component │
│ Auth Service │
│ HTTP Interceptor │
└──────────┬───────────┘
│
│ Username + Password
▼
┌──────────────────────────────┐
│ ASP.NET Core Web API │
│ │
│ AuthController │
│ ↓ │
│ Validate User │
│ ↓ │
│ Generate JWT │
└─────────────┬────────────────┘
│
│ JWT
▼
┌──────────────────────┐
│ Angular │
│ │
│ Store/use JWT │
└──────────┬───────────┘
│
│ API Request
│
│ Authorization:
│ Bearer <JWT>
▼
┌──────────────────────────────┐
│ ASP.NET Core Web API │
│ │
│ JWT Bearer Middleware │
│ ↓ │
│ Validate JWT │
│ ↓ │
│ [Authorize] │
│ ↓ │
│ ProductsController │
└──────────────────────────────┘
13. What Happens When the Token Expires?
Suppose the token is created with:
expires:
DateTime.UtcNow.AddMinutes(30)
After 30 minutes, the access token expires.
Angular sends:
Authorization: Bearer <expired-token>
The API rejects it:
401 Unauthorized
A production application commonly uses:
Access Token
+
Refresh Token
The flow then becomes:
Access Token expires
↓
Angular receives 401
↓
Send Refresh Token
↓
Web API validates Refresh Token
↓
Generate new Access Token
↓
Angular retries/continues requests
That is more complete than the simple demo above.
14. 401 vs 403
This is an important interview distinction.
401 Unauthorized
Authentication failed.
Examples:
JWT missing
JWT invalid
JWT expired
Result:
401 Unauthorized
403 Forbidden
The user is authenticated, but does not have permission.
For example:
[Authorize(Roles = "Admin")]
User's token contains:
Role = User
The token itself may be valid, but the user isn't authorized for that resource.
Result:
403 Forbidden
So remember:
401 → Who are you? Authentication problem
403 → I know who you are,
but you don't have permission.
15. Key Points
- JWT is commonly used for token-based authentication between Angular and ASP.NET Core APIs.
- Angular sends credentials to a login endpoint.
- ASP.NET Core validates the credentials.
- ASP.NET Core generates a signed JWT.
- JWT can contain claims such as user ID, username, and roles.
- Angular receives the access token.
- An Angular HTTP interceptor can attach the token automatically.
- The standard header is:
Authorization: Bearer <token>
- ASP.NET Core JWT Bearer authentication validates the token.
[Authorize]protects API endpoints.[Authorize(Roles = "Admin")]performs role-based authorization.- An expired or invalid token normally results in 401.
- An authenticated user without sufficient authorization normally gets 403.
- Production systems commonly use short-lived access tokens and, where appropriate, refresh tokens.
- Always use HTTPS.
- Do not store plaintext passwords.
- Protect signing keys using a proper secret-management mechanism.
- Browser token storage requires careful XSS/CSRF threat modeling;
localStorageis convenient but is not automatically the safest choice for every application.
16. Interview Questions and Answers
1. How does JWT authentication work between Angular and ASP.NET Core Web API?
Angular sends login credentials to the Web API. After validating the user, the API generates a signed JWT. Angular sends that JWT in the Authorization: Bearer <token> header for subsequent requests, and ASP.NET Core validates it before allowing access to protected endpoints.
2. Where is JWT generated?
The JWT is normally generated by the backend authentication system, not by Angular.
ASP.NET Core / Identity Provider
↓
Generate JWT
↓
Angular receives JWT
3. How does Angular send JWT to the API?
Using the HTTP Authorization header:
Authorization: Bearer <token>
An Angular interceptor is commonly used to add this automatically.
4. Why use an Angular HTTP interceptor?
It centralizes authentication-header logic.
Instead of doing this in every service:
Authorization: `Bearer ${token}`
the interceptor handles it automatically.
5. What does [Authorize] do?
[Authorize]
restricts an endpoint to users who satisfy the configured authorization requirements, with authentication typically established by the JWT bearer handler.
6. What is stored inside a JWT?
A JWT can contain claims such as:
User ID
Username
Role
Issuer
Audience
Expiration
Sensitive secrets such as passwords should not be placed in the JWT payload.
7. Is a JWT encrypted?
Not normally.
A typical JWT used in this flow is signed, not encrypted. Its payload can generally be decoded by anyone who possesses the token.
The signature protects integrity/authenticity, not confidentiality.
8. What happens when JWT expires?
The API rejects the expired access token, typically with:
401 Unauthorized
The application can require the user to log in again or use a refresh-token flow.
9. What is the difference between 401 and 403?
401 → Authentication failed.
403 → Authentication succeeded,
but authorization failed.
10. Should Angular validate JWT security?
Angular can inspect expiration or claims for UI behavior, such as hiding an Admin menu.
But Angular must never be the security boundary.
The ASP.NET Core API must enforce authentication and authorization.
Angular guard
↓
User experience/navigation
ASP.NET Core [Authorize]
↓
Actual security
11. Can a route guard replace [Authorize]?
No.
An Angular route guard only controls client-side navigation. A user can bypass the Angular application and call the API directly.
Therefore:
Angular Guard → UI/navigation protection
ASP.NET [Authorize] → Server-side API protection
The backend authorization is mandatory for real security.
12. Why are issuer and audience validated?
Issuer verifies who issued the token.
Audience verifies whom the token was intended for.
Issuer → Who created this token?
Audience → Who should accept this token?
13. What is the JWT signing key used for?
The signing key is used to create or validate the token's cryptographic signature.
It helps the API detect whether the token was modified or was not signed by a trusted issuer.
Interview Quick Revision
The entire concept can be remembered as:
1. Angular sends username/password
↓
2. ASP.NET Core validates user
↓
3. API generates signed JWT
↓
4. Angular receives token
↓
5. Angular interceptor attaches token
↓
6. Authorization: Bearer <JWT>
↓
7. ASP.NET Core validates JWT
↓
8. [Authorize] checks access
↓
9. Controller executes
One-line interview answer:
Angular authenticates the user against the ASP.NET Core Web API, receives a JWT, and sends that token in the Bearer authorization header on subsequent requests. ASP.NET Core validates the token and uses
[Authorize]and claims/roles to control access to protected API endpoints.