Angular Gets 401 Unauthorized from ASP.NET Core — How to Troubleshoot
1. What is 401 Unauthorized?
A 401 Unauthorized response means the ASP.NET Core API could not successfully authenticate the request.
In a typical Angular + ASP.NET Core JWT application:
Angular
|
| Authorization: Bearer <JWT>
v
ASP.NET Core Web API
|
|-- Is token present?
|-- Is token valid?
|-- Is token expired?
|-- Is signature valid?
|-- Are issuer/audience valid?
v
Controller
A useful distinction:
| Status | Meaning |
|---|---|
401 Unauthorized |
Authentication failed — API cannot establish who the caller is |
403 Forbidden |
Authentication succeeded, but the caller lacks permission |
So for a 401, troubleshoot authentication first, not Angular routing or UI authorization.
2. Troubleshooting Order
I would investigate a 401 in this order:
1. Did login succeed?
↓
2. Did Angular receive a JWT?
↓
3. Is Angular storing/retaining the token correctly?
↓
4. Is Angular sending Authorization: Bearer <token>?
↓
5. Is an interceptor modifying or omitting it?
↓
6. Is the token expired?
↓
7. Does ASP.NET Core validate the same issuer/audience/signing key?
↓
8. Is authentication middleware configured correctly?
↓
9. Is [Authorize] requiring a policy/scheme that differs?
The browser Network tab is usually the fastest starting point.
3. Full Simple Sample Program
Assume we have:
- Angular frontend
- ASP.NET Core Web API
- JWT authentication
/api/auth/loginreturns a token/api/productsrequires authentication
ASP.NET Core — appsettings.json
{
"Jwt": {
"Key": "ThisIsMySuperSecretKeyForJwtAuthentication12345",
"Issuer": "MyApi",
"Audience": "MyAngularApp"
}
}
ASP.NET Core — 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"]!;
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.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
The order is important:
app.UseAuthentication();
app.UseAuthorization();
Authentication should happen before authorization.
4. Login API
AuthController.cs
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)
{
if (request.Username != "admin" ||
request.Password != "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(30),
signingCredentials: credentials);
var tokenString =
new JwtSecurityTokenHandler()
.WriteToken(token);
return Ok(new
{
token = tokenString
});
}
}
public record LoginRequest(
string Username,
string Password);
5. Protected API
ProductsController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ProductsController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok(new[]
{
new
{
Id = 1,
Name = "Laptop"
},
new
{
Id = 2,
Name = "Mouse"
}
});
}
}
Because of:
[Authorize]
the endpoint requires authentication.
6. Angular Login Service
auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs';
interface LoginResponse {
token: string;
}
@Injectable({
providedIn: 'root'
})
export class AuthService {
private apiUrl = 'https://localhost:7001/api';
constructor(private http: HttpClient) {}
login(username: string, password: string) {
return this.http
.post<LoginResponse>(
`${this.apiUrl}/auth/login`,
{
username,
password
}
)
.pipe(
tap(response => {
localStorage.setItem(
'access_token',
response.token
);
})
);
}
getToken(): string | null {
return localStorage.getItem('access_token');
}
logout(): void {
localStorage.removeItem('access_token');
}
}
After login, Angular stores:
access_token
7. Angular JWT Interceptor
Using a modern functional 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);
};
The important part is:
Authorization: `Bearer ${token}`
The HTTP request should look like:
GET /api/products
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
8. Register the Interceptor
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
])
)
]
};
Without registering the interceptor:
withInterceptors([
authInterceptor
])
your interceptor will never run.
That can easily result in:
401 Unauthorized
9. Angular Product Service
product.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export interface Product {
id: number;
name: string;
}
@Injectable({
providedIn: 'root'
})
export class ProductService {
private apiUrl =
'https://localhost:7001/api/products';
constructor(private http: HttpClient) {}
getProducts() {
return this.http.get<Product[]>(
this.apiUrl
);
}
}
You don't need to manually add the JWT here.
The interceptor automatically converts:
GET /api/products
into:
GET /api/products
Authorization: Bearer eyJ...
10. First Check — Browser Network Tab
This is the first thing I would check.
Open:
F12
↓
Network
↓
Select failing request
↓
Headers
Look at Request Headers.
You should see:
Authorization: Bearer eyJhbGciOi...
If you see no Authorization header, the problem is probably on the Angular side.
Check:
Login
↓
Token received?
↓
Token retained?
↓
Interceptor running?
↓
Authorization header attached?
11. Check Whether Angular Has the Token
Temporarily inspect:
const token =
localStorage.getItem('access_token');
console.log(token);
Expected:
eyJhbGciOiJIUzI1NiIsInR5cCI...
If:
null
then investigate login/token storage before investigating the API.
For production security design, token storage deserves separate consideration; this example uses localStorage only to keep the authentication flow simple.
12. Check the Bearer Prefix
This is correct:
Authorization: `Bearer ${token}`
This is wrong:
Authorization: token
Also wrong:
Authorization: `JWT ${token}`
Expected format:
Authorization: Bearer <JWT>
Notice the space after:
Bearer
13. Check Token Expiration
JWTs commonly contain:
{
"exp": 1790952000
}
ASP.NET Core has:
ValidateLifetime = true
If the token is expired, authentication fails.
The application normally needs a strategy such as:
Access Token expires
↓
Refresh Token
↓
Request new Access Token
↓
Retry / continue API calls
Or the application can redirect the user to login when authentication can no longer be refreshed.
14. Check Issuer
Suppose the token was generated using:
issuer: "MyApi"
Then validation must expect:
ValidIssuer = "MyApi"
This mismatch fails:
Token:
iss = MyApi
API expects:
ValidIssuer = DifferentApi
Result:
401
15. Check Audience
Token:
audience: "MyAngularApp"
Validation:
ValidAudience = "MyAngularApp"
If they don't match, token validation fails.
For example:
JWT
aud = MyAngularApp
API
ValidAudience = PortfolioAngularApp
↓
Audience validation fails
↓
401
16. Check the Signing Key
This is extremely important.
Token generation:
var key =
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtKey));
Token validation:
IssuerSigningKey =
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtKey));
They must use the same signing secret/key material.
For example:
Token generated using:
ABC123...
API validates using:
XYZ456...
↓
Invalid signature
↓
401
17. Check ASP.NET Core Middleware
Make sure you have:
app.UseAuthentication();
app.UseAuthorization();
in the appropriate middleware order.
Conceptually:
HTTP Request
↓
Authentication
↓
Create ClaimsPrincipal
↓
Authorization
↓
Controller
If authentication isn't configured/executed correctly, [Authorize] cannot authenticate the request.
18. Check Authentication Scheme
Configuration should typically specify JWT Bearer as the authentication scheme:
builder.Services
.AddAuthentication(
JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(...);
That tells ASP.NET Core that JWT Bearer authentication is the default scheme.
In applications with multiple schemes—such as cookies plus JWT—scheme selection becomes especially important.
19. Check [Authorize]
Simple authentication:
[Authorize]
But you might have:
[Authorize(Roles = "Admin")]
or:
[Authorize(Policy = "AdminOnly")]
or a specific scheme:
[Authorize(
AuthenticationSchemes =
JwtBearerDefaults.AuthenticationScheme)]
Check what the endpoint actually requires.
Remember, a caller that is successfully authenticated but lacks the required role/policy will normally result in 403, whereas failure to authenticate produces 401.
20. Test the API Independently
A very useful isolation technique is to test the JWT outside Angular.
For example:
Login API
↓
Copy JWT
↓
Swagger / Postman
↓
Authorization: Bearer <token>
↓
GET /api/products
Scenario 1 — Postman works, Angular fails
Then investigate:
Angular
├── token storage/retention
├── interceptor
├── Authorization header
└── request configuration
Scenario 2 — Postman also returns 401
Then investigate:
ASP.NET Core
├── JWT configuration
├── issuer
├── audience
├── signing key
├── expiration
├── authentication scheme
└── middleware
This is an excellent debugging technique because it separates the frontend from the backend.
21. Enable JWT Authentication Diagnostics
During development, you can add JWT Bearer events:
.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))
};
options.Events =
new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
Console.WriteLine(
$"JWT authentication failed: " +
$"{context.Exception.Message}");
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
Console.WriteLine(
"JWT validated successfully.");
return Task.CompletedTask;
}
};
});
This can reveal problems such as:
token expired
invalid audience
invalid issuer
signature validation failed
Avoid returning detailed token-validation errors to clients in production; keep diagnostics in secure server-side logs.
22. What About CORS?
Don't automatically assume:
401 = CORS
They are different problems.
401
Authentication failed
CORS
Browser's cross-origin access rules rejected/blocked the interaction
However, browser requests can involve both CORS and authentication, so inspect the Network tab carefully, including whether a preflight request is failing.
23. Add a Global Angular 401 Handler
You can also handle 401 centrally.
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) {
localStorage.removeItem(
'access_token'
);
router.navigate(['/login']);
}
return throwError(() => error);
})
);
};
Register both:
provideHttpClient(
withInterceptors([
authInterceptor,
errorInterceptor
])
)
Now the flow becomes:
Angular request
↓
authInterceptor
↓
Add JWT
↓
ASP.NET Core
↓
401
↓
errorInterceptor
↓
Clear invalid token
↓
Login page
In a refresh-token application, you would usually attempt a controlled token refresh before forcing login.
24. Practical Troubleshooting Checklist
When Angular gets a 401, I would check:
| Check | What to verify |
|---|---|
| Login | Did authentication succeed? |
| Token response | Did API actually return a JWT? |
| Token retention | Is Angular retaining the correct token? |
| Network tab | Is Authorization present? |
| Header format | Bearer <token> |
| Interceptor | Is it registered and executing? |
| Expiration | Has JWT expired? |
| Issuer | Token iss matches API |
| Audience | Token aud matches API |
| Signature | Correct signing key |
| Scheme | JWT Bearer configured correctly |
| Middleware | Authentication before authorization |
| Endpoint | Correct [Authorize] requirements |
| Independent test | Does same token work in Swagger/Postman? |
| Server logs | What specifically caused JWT validation failure? |
Key Points
401primarily means authentication failed.- Start with the browser Network tab.
- Verify:
Authorization: Bearer <token>
- If the header is missing, investigate Angular.
- If the header is present, investigate token validity and ASP.NET Core JWT configuration.
- Verify:
- expiration
- issuer
- audience
- signature/signing key
- authentication scheme
- Test the same token with Swagger/Postman to isolate frontend vs backend.
- Use server-side JWT authentication diagnostics during development.
401and403are different:
401 → Who are you? Authentication failed.
403 → I know who you are, but you aren't allowed.
Interview Questions and Answers
1. Your Angular application gets a 401 from ASP.NET Core. How would you troubleshoot it?
Answer:
I would first inspect the browser Network tab and verify that Angular is sending
Authorization: Bearer <token>. Then I would check whether the token is expired and verify the issuer, audience, signing key, and JWT Bearer configuration in ASP.NET Core. Finally, I would test the same token through Swagger or Postman to determine whether the issue is in Angular or the API.
2. What is the difference between 401 and 403?
Answer:
401 indicates that authentication failed or valid authentication credentials were not supplied.
403 means the caller is authenticated but does not have sufficient authorization to access the resource.
3. Postman works, but Angular gets 401. What would you check?
Answer:
I would primarily investigate the Angular request:
Token available?
↓
Interceptor registered?
↓
Authorization header added?
↓
Correct Bearer format?
I would compare Angular's actual request headers against the working Postman request.
4. Both Angular and Postman return 401. Where is the likely problem?
Answer:
That points more strongly toward the token or backend authentication configuration. I would verify expiration, issuer, audience, signing key, authentication scheme, and JWT Bearer configuration.
5. Can an expired JWT produce 401?
Yes.
When:
ValidateLifetime = true
ASP.NET Core rejects an expired token during authentication.
6. Should Angular manually attach JWTs in every service?
Usually no.
A centralized HTTP interceptor is cleaner:
Authorization: `Bearer ${token}`
This avoids duplicating authentication code across services.
7. Should Angular route guards be used instead of ASP.NET Core [Authorize]?
No.
Angular guards improve client-side navigation and user experience, but they do not provide backend security.
The API must independently enforce authorization:
[Authorize]
The important architecture is:
Angular Guard
↓
Client-side navigation control
Angular Interceptor
↓
Attach JWT
ASP.NET Core Authentication
↓
Validate JWT
ASP.NET Core Authorization
↓
Enforce roles/policies
Controller
The backend remains the security boundary.