Login Using Angular + ASP.NET Core JWT Authentication
1. What is JWT Login?
In an Angular + ASP.NET Core Web API application, Angular sends the user's credentials to the API. ASP.NET Core validates them and, if valid, generates a JSON Web Token (JWT).
Angular then sends that token with protected API requests:
Authorization: Bearer <JWT>
The overall flow is:
Angular Login Form
↓
POST /api/auth/login
↓
ASP.NET Core validates credentials
↓
Generate JWT
↓
Return JWT to Angular
↓
Angular retains token
↓
HTTP Interceptor attaches token
↓
Authorization: Bearer <token>
↓
ASP.NET Core validates JWT
↓
[Authorize]
↓
Protected API
For a simple learning example, I'll use hard-coded credentials and
localStorage. In a production application, credentials should come from a database with properly hashed passwords, and token storage/refresh strategy should be designed carefully.
2. Project Structure
ASP.NET Core
MyApi
│
├── Controllers
│ ├── AuthController.cs
│ └── ProductsController.cs
│
├── Models
│ └── LoginRequest.cs
│
├── appsettings.json
└── Program.cs
Angular
src/app
│
├── login
│ ├── login.ts
│ └── login.html
│
├── products
│ ├── products.ts
│ └── products.html
│
├── services
│ └── auth.service.ts
│
├── interceptors
│ └── auth.interceptor.ts
│
├── guards
│ └── auth.guard.ts
│
├── app.routes.ts
└── app.config.ts
Part 1 — ASP.NET Core Web API
3. Install JWT Package
Install:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
4. JWT Configuration
appsettings.json
{
"Jwt": {
"Key": "ThisIsMySuperSecretKeyForJwtAuthentication12345",
"Issuer": "MyApi",
"Audience": "MyAngularApp",
"ExpiryMinutes": 30
}
}
These values mean:
| Setting | Purpose |
|---|---|
Key |
Signs the JWT |
Issuer |
Who generated the token |
Audience |
Who the token is intended for |
ExpiryMinutes |
Token lifetime |
The signing key should be stored securely in production rather than committed to source control.
5. Login Request Model
LoginRequest.cs
namespace MyApi.Models;
public class LoginRequest
{
public string Username { get; set; } = "";
public string Password { get; set; } = "";
}
Angular will send:
{
"username": "admin",
"password": "123"
}
6. Configure JWT Authentication
Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddCors(options =>
{
options.AddPolicy("Angular", policy =>
{
policy
.WithOrigins("http://localhost:4200")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
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("Angular");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
The critical part is:
app.UseAuthentication();
app.UseAuthorization();
Authentication identifies the caller; authorization determines whether that authenticated caller can access a resource.
7. Create the Login API
AuthController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using MyApi.Models;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace MyApi.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.
if (request.Username != "admin" ||
request.Password != "123")
{
return Unauthorized(new
{
message = "Invalid username or password"
});
}
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 expiryMinutes =
int.Parse(
_configuration["Jwt:ExpiryMinutes"]!);
var token =
new JwtSecurityToken(
issuer:
_configuration["Jwt:Issuer"],
audience:
_configuration["Jwt:Audience"],
claims: claims,
expires:
DateTime.UtcNow
.AddMinutes(expiryMinutes),
signingCredentials:
credentials);
var tokenString =
new JwtSecurityTokenHandler()
.WriteToken(token);
return Ok(new
{
token = tokenString
});
}
}
8. What Happens During Login?
Angular sends:
POST /api/auth/login
Content-Type: application/json
Body:
{
"username": "admin",
"password": "123"
}
ASP.NET Core checks:
if (request.Username != "admin" ||
request.Password != "123")
{
return Unauthorized();
}
If credentials are correct, claims are created:
new Claim(
ClaimTypes.Name,
request.Username),
new Claim(
ClaimTypes.Role,
"Admin")
The JWT therefore contains identity information such as:
User = admin
Role = Admin
9. JWT Generation
The signing key:
var key =
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(
_configuration["Jwt:Key"]!));
Signing credentials:
var credentials =
new SigningCredentials(
key,
SecurityAlgorithms.HmacSha256);
Then:
var token =
new JwtSecurityToken(
issuer: "...",
audience: "...",
claims: claims,
expires: ...,
signingCredentials: credentials);
Finally:
var tokenString =
new JwtSecurityTokenHandler()
.WriteToken(token);
Response:
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}
10. Create a Protected API
ProductsController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace MyApi.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ProductsController : ControllerBase
{
[HttpGet]
public IActionResult GetProducts()
{
return Ok(new[]
{
new
{
Id = 1,
Name = "Laptop",
Price = 50000
},
new
{
Id = 2,
Name = "Mouse",
Price = 1000
}
});
}
}
Because of:
[Authorize]
this request requires a valid JWT.
Without a valid token:
401 Unauthorized
With a valid token:
200 OK
Part 2 — Angular
11. Configure HttpClient
Modern standalone Angular uses:
provideHttpClient()
instead of importing HttpClientModule into an AppModule.
app.config.ts
import {
ApplicationConfig
} from '@angular/core';
import {
provideHttpClient,
withInterceptors
} from '@angular/common/http';
import {
provideRouter
} from '@angular/router';
import {
routes
} from './app.routes';
import {
authInterceptor
} from './interceptors/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(
withInterceptors([
authInterceptor
])
)
]
};
12. Create Angular Authentication Service
auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs';
interface LoginRequest {
username: string;
password: string;
}
interface LoginResponse {
token: string;
}
@Injectable({
providedIn: 'root'
})
export class AuthService {
private apiUrl =
'https://localhost:7001/api/auth';
constructor(
private http: HttpClient
) {}
login(username: string, password: string) {
const request: LoginRequest = {
username,
password
};
return this.http
.post<LoginResponse>(
`${this.apiUrl}/login`,
request
)
.pipe(
tap(response => {
localStorage.setItem(
'access_token',
response.token
);
})
);
}
getToken(): string | null {
return localStorage.getItem(
'access_token'
);
}
isLoggedIn(): boolean {
return !!this.getToken();
}
logout(): void {
localStorage.removeItem(
'access_token'
);
}
}
13. Login Component
login.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import {
AuthService
} from '../services/auth.service';
@Component({
selector: 'app-login',
standalone: true,
imports: [
FormsModule
],
templateUrl: './login.html'
})
export class Login {
username = '';
password = '';
errorMessage = '';
constructor(
private authService: AuthService,
private router: Router
) {}
login(): void {
this.errorMessage = '';
this.authService
.login(
this.username,
this.password
)
.subscribe({
next: () => {
this.router.navigate([
'/products'
]);
},
error: error => {
if (error.status === 401) {
this.errorMessage =
'Invalid username or password';
} else {
this.errorMessage =
'Login failed';
}
}
});
}
}
14. Login HTML
login.html
<h2>Login</h2>
<form (ngSubmit)="login()">
<div>
<label>Username</label>
<input
type="text"
name="username"
[(ngModel)]="username">
</div>
<div>
<label>Password</label>
<input
type="password"
name="password"
[(ngModel)]="password">
</div>
<button type="submit">
Login
</button>
</form>
@if (errorMessage) {
<p>{{ errorMessage }}</p>
}
Enter:
Username: admin
Password: 123
Angular sends:
POST https://localhost:7001/api/auth/login
15. What Happens After Successful Login?
ASP.NET Core returns:
{
"token": "eyJhbGciOiJIUzI1Ni..."
}
The service executes:
localStorage.setItem(
'access_token',
response.token
);
Now:
Browser
↓
localStorage
↓
access_token
↓
eyJhbGciOiJIUzI1Ni...
16. Create JWT Interceptor
We don't want every Angular service to manually do this:
headers: {
Authorization: `Bearer ${token}`
}
Instead, use an interceptor.
auth.interceptor.ts
import {
HttpInterceptorFn
} from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn =
(req, next) => {
const token =
localStorage.getItem(
'access_token'
);
if (!token) {
return next(req);
}
const authRequest =
req.clone({
setHeaders: {
Authorization:
`Bearer ${token}`
}
});
return next(authRequest);
};
Now Angular automatically adds:
Authorization: Bearer eyJhbGciOi...
17. Product Component
products.ts
import {
Component,
OnInit
} from '@angular/core';
import {
HttpClient
} from '@angular/common/http';
interface Product {
id: number;
name: string;
price: number;
}
@Component({
selector: 'app-products',
standalone: true,
templateUrl: './products.html'
})
export class Products implements OnInit {
products: Product[] = [];
constructor(
private http: HttpClient
) {}
ngOnInit(): void {
this.http
.get<Product[]>(
'https://localhost:7001/api/products'
)
.subscribe(products => {
this.products = products;
});
}
}
Notice that there is no JWT code here.
The interceptor handles it.
18. Product HTML
products.html
<h2>Products</h2>
@for (product of products; track product.id) {
<div>
{{ product.name }}
-
{{ product.price }}
</div>
}
19. Create an Authentication Guard
The interceptor attaches the JWT to HTTP requests.
A route guard controls Angular navigation.
auth.guard.ts
import {
inject
} from '@angular/core';
import {
CanActivateFn,
Router
} from '@angular/router';
import {
AuthService
} from '../services/auth.service';
export const authGuard: CanActivateFn =
() => {
const authService =
inject(AuthService);
const router =
inject(Router);
if (authService.isLoggedIn()) {
return true;
}
return router.createUrlTree([
'/login'
]);
};
20. Configure Routes
app.routes.ts
import {
Routes
} from '@angular/router';
import {
authGuard
} from './guards/auth.guard';
export const routes: Routes = [
{
path: 'login',
loadComponent: () =>
import('./login/login')
.then(m => m.Login)
},
{
path: 'products',
canActivate: [
authGuard
],
loadComponent: () =>
import('./products/products')
.then(m => m.Products)
},
{
path: '',
redirectTo: 'login',
pathMatch: 'full'
}
];
Now:
/products
↓
authGuard
↓
Logged in?
/ \
Yes No
↓ ↓
Open /login
But remember:
Angular route guards are not a security boundary.
A user can manipulate browser-side JavaScript.
Actual security is enforced by:
[Authorize]
on the ASP.NET Core API.
21. Complete Login Flow
The complete architecture is:
USER
|
v
Angular Login Form
|
| username/password
v
POST /api/auth/login
|
v
ASP.NET Core API
|
| Validate user
v
Generate JWT
|
v
Return JWT
|
v
Angular AuthService
|
| retain token
v
Browser token state
|
v
Navigate /products
|
v
authGuard
|
v
Products component
|
| GET /api/products
v
authInterceptor
|
| Add:
| Authorization:
| Bearer <JWT>
v
ASP.NET Core API
|
v
JWT Authentication
|
v
[Authorize]
|
v
ProductsController
|
v
200 OK
22. Authentication vs Authorization
This distinction is important in interviews.
Authentication
Who are you?
Example:
Username/password
↓
JWT
↓
Authenticated user
Authorization
What are you allowed to do?
Example:
[Authorize(Roles = "Admin")]
The user might be authenticated but not be an administrator.
Then the typical response is:
403 Forbidden
23. Role-Based Authorization Example
We created:
new Claim(
ClaimTypes.Role,
"Admin")
Therefore we can protect an endpoint:
[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
return Ok();
}
Flow:
JWT
↓
Role = Admin
↓
[Authorize(Roles = "Admin")]
↓
Allowed
24. What About Logout?
JWT authentication is generally stateless.
For our simple example:
logout(): void {
localStorage.removeItem(
'access_token'
);
}
Then redirect:
this.authService.logout();
this.router.navigate([
'/login'
]);
The browser no longer has the access token to send.
More advanced systems may also revoke/rotate refresh tokens server-side.
25. What About Token Expiration?
Our API creates the token with:
expires:
DateTime.UtcNow
.AddMinutes(30)
After 30 minutes:
JWT expires
↓
Angular sends expired JWT
↓
ASP.NET Core validates lifetime
↓
Authentication fails
↓
401 Unauthorized
A production application commonly uses:
Short-lived Access Token
+
Longer-lived Refresh Token
Flow:
Access token expires
↓
API returns 401
↓
Angular attempts refresh
↓
Refresh endpoint
↓
New access token
↓
Continue requests
26. Important Production Security Consideration
For learning, we used:
localStorage.setItem(
'access_token',
token
);
This is simple, but localStorage is accessible to JavaScript running on the page. Therefore, an XSS vulnerability can expose the token.
A production design should deliberately choose its authentication architecture.
For example, depending on the application:
Option A
Short-lived JWT access token
+ refresh-token strategy
Option B
Secure HttpOnly cookies
+ CSRF protection
Don't assume localStorage is automatically the correct production choice simply because it is easy to implement.
Also:
- Never store passwords in Angular.
- Never put JWT signing secrets in Angular.
- Never trust client-side role checks as backend security.
- Always use HTTPS in production.
- Hash user passwords server-side using an appropriate password-hashing mechanism.
Key Points
ASP.NET Core responsibilities
Validate username/password
Generate JWT
Sign JWT
Set expiration
Add claims
Validate JWT
Perform authorization
Protect APIs using [Authorize]
Angular responsibilities
Collect credentials
Call login API
Retain authentication state/token
Attach JWT using interceptor
Handle 401 responses
Use guards for navigation UX
Logout
Most important architecture
Angular
|
| POST username/password
v
Login API
|
| JWT
v
Angular
|
| Authorization: Bearer JWT
v
ASP.NET Core Authentication
|
v
[Authorize]
|
v
Protected API
And remember:
Angular Guard
=
Navigation convenience
ASP.NET Core [Authorize]
=
Actual API security
Interview Questions and Answers
1. How would you implement login using Angular and ASP.NET Core JWT authentication?
Interview Answer:
Angular sends the username and password to an ASP.NET Core login endpoint. The API validates the credentials, generates a signed JWT containing the required claims, and returns it. Angular retains the token and an HTTP interceptor attaches it as
Authorization: Bearer <token>to protected API requests. ASP.NET Core validates the JWT and protects endpoints using[Authorize].
2. Why use an Angular HTTP interceptor?
An interceptor centralizes token attachment:
Authorization: Bearer <token>
This avoids repeating authentication-header code in every Angular service.
3. What does [Authorize] do?
[Authorize]
requires the request to have a successfully authenticated user.
With JWT Bearer authentication, ASP.NET Core validates the JWT before allowing access.
4. Where should the JWT signing key be stored?
The signing key belongs on the server side only.
Production secrets should normally be stored in a secure configuration mechanism such as environment-specific secret storage or a managed secrets service.
Never place the JWT signing key in Angular.
5. What happens when the JWT expires?
ASP.NET Core rejects the expired access token, normally resulting in:
401 Unauthorized
The application can then authenticate again or use a refresh-token mechanism to obtain a new access token.
6. Should Angular route guards provide security?
No.
Angular Guard
↓
Controls client navigation
ASP.NET Core [Authorize]
↓
Enforces actual API security
Backend authorization must always be enforced independently.
7. 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.
8. What is the difference between 401 and 403?
401 Unauthorized
→ Authentication failed.
403 Forbidden
→ Authentication succeeded,
but permission is insufficient.
9. Why shouldn't the JWT signing key be stored in Angular?
Angular code is downloaded to the user's browser and can be inspected.
If the signing key were exposed, an attacker could potentially create forged tokens.
The signing key must remain on the trusted server side.
10. What would you improve for production?
Interview Answer:
I would validate users against a database with securely hashed passwords, use HTTPS, short-lived access tokens, an appropriate refresh-token strategy, secure server-side secret management, backend role/policy authorization, centralized 401 handling, and a deliberate token-storage strategy based on the application's security requirements.
For interview revision, the most important flow to remember is:
Login
↓
Validate Credentials
↓
Generate JWT
↓
Return JWT
↓
Angular retains JWT
↓
Interceptor adds Bearer token
↓
ASP.NET Core validates JWT
↓
[Authorize]
↓
Protected API