How Do You Secure Sensitive Routes in Angular?
In an Angular + ASP.NET Core application, sensitive routes such as:
/admin
/admin/users
/dashboard
/profile
/orders
should be protected at two levels:
Angular Route Guard
+
ASP.NET Core Authorization
The most important principle is:
Angular route guards improve client-side navigation and UX, but ASP.NET Core must enforce the real security.
A user can bypass Angular and call your API directly using Postman, curl, or another client.
1. Full Simple Sample Program
Suppose we have:
/login→ public/dashboard→ authenticated users/admin→ Admin role only
auth.service.ts
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AuthService {
isLoggedIn = signal(false);
role = signal<string | null>(null);
login(role: string): void {
this.isLoggedIn.set(true);
this.role.set(role);
}
logout(): void {
this.isLoggedIn.set(false);
this.role.set(null);
}
}
For simplicity, this demo keeps authentication state in signals. In a real application, the state should reflect your actual authenticated session/token and backend responses.
2. Authentication Guard
auth.guard.ts
import { inject } from '@angular/core';
import {
CanActivateFn,
Router
} from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn =
(route, state) => {
const authService =
inject(AuthService);
const router =
inject(Router);
if (authService.isLoggedIn()) {
return true;
}
return router.createUrlTree(
['/login'],
{
queryParams: {
returnUrl: state.url
}
}
);
};
3. Admin Authorization Guard
admin.guard.ts
import { inject } from '@angular/core';
import {
CanActivateFn,
Router
} from '@angular/router';
import { AuthService } from './auth.service';
export const adminGuard: CanActivateFn =
() => {
const authService =
inject(AuthService);
const router =
inject(Router);
if (
authService.isLoggedIn() &&
authService.role() === 'Admin'
) {
return true;
}
return router.createUrlTree(
['/unauthorized']
);
};
4. Configure Routes
app.routes.ts
import { Routes } from '@angular/router';
import { Login } from './login';
import { Dashboard } from './dashboard';
import { Admin } from './admin';
import { Unauthorized } from './unauthorized';
import { authGuard } from './auth.guard';
import { adminGuard } from './admin.guard';
export const routes: Routes = [
{
path: 'login',
component: Login
},
{
path: 'dashboard',
component: Dashboard,
canActivate: [authGuard]
},
{
path: 'admin',
component: Admin,
canActivate: [
authGuard,
adminGuard
]
},
{
path: 'unauthorized',
component: Unauthorized
},
{
path: '',
redirectTo: 'login',
pathMatch: 'full'
}
];
5. Register Router
app.config.ts
import {
ApplicationConfig
} from '@angular/core';
import {
provideRouter
} from '@angular/router';
import { routes } from './app.routes';
export const appConfig:
ApplicationConfig = {
providers: [
provideRouter(routes)
]
};
6. Login Component
login.ts
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';
@Component({
selector: 'app-login',
standalone: true,
templateUrl: './login.html'
})
export class Login {
private authService =
inject(AuthService);
private router =
inject(Router);
loginAsUser(): void {
this.authService.login('User');
this.router.navigate(
['/dashboard']
);
}
loginAsAdmin(): void {
this.authService.login('Admin');
this.router.navigate(
['/admin']
);
}
}
login.html
<h2>Login</h2>
<button (click)="loginAsUser()">
Login as User
</button>
<button (click)="loginAsAdmin()">
Login as Admin
</button>
7. Dashboard Component
dashboard.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-dashboard',
standalone: true,
template: `
<h2>Dashboard</h2>
<p>
Only authenticated users should
access this page.
</p>
`
})
export class Dashboard {
}
8. Admin Component
admin.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-admin',
standalone: true,
template: `
<h2>Admin Dashboard</h2>
<p>
Only administrators should
access this page.
</p>
`
})
export class Admin {
}
9. Unauthorized Component
unauthorized.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-unauthorized',
standalone: true,
template: `
<h2>403 - Access Denied</h2>
<p>
You do not have permission
to access this page.
</p>
`
})
export class Unauthorized {
}
10. How the Authentication Guard Works
When the user enters:
/dashboard
Angular checks:
canActivate: [authGuard]
The flow becomes:
User
↓
/dashboard
↓
Angular Router
↓
authGuard
↓
Is authenticated?
/ \
Yes No
↓ ↓
Dashboard /login
The guard prevents normal Angular navigation to the protected route.
11. How Role-Based Route Protection Works
For:
/admin
we configured:
canActivate: [
authGuard,
adminGuard
]
The conceptual flow is:
/admin
↓
authGuard
↓
Logged in?
↓
adminGuard
↓
Role == Admin?
/ \
Yes No
↓ ↓
Admin /unauthorized
This provides client-side authorization behavior.
12. But Angular Guards Are NOT Real Security
This is the most important point.
Suppose Angular protects:
/admin/users
with:
canActivate: [adminGuard]
but the backend exposes:
DELETE /api/users/10
without authorization.
An attacker doesn't have to use your Angular application.
They could attempt:
Postman
↓
DELETE /api/users/10
↓
ASP.NET Core
The Angular guard isn't involved at all.
Therefore:
Angular Guard
↓
Client-side navigation protection
ASP.NET Core [Authorize]
↓
Actual server-side security
13. Secure the ASP.NET Core API
For authenticated users:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ProfileController : ControllerBase
{
[HttpGet]
public IActionResult GetProfile()
{
return Ok("Profile information");
}
}
Now anonymous users cannot access the endpoint successfully.
14. Role-Based Backend Authorization
For admin functionality:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
[Authorize(Roles = "Admin")]
public class AdminController : ControllerBase
{
[HttpGet("users")]
public IActionResult GetUsers()
{
return Ok("User list");
}
[HttpDelete("users/{id}")]
public IActionResult DeleteUser(int id)
{
return Ok(
$"User {id} deleted"
);
}
}
Now the server itself checks:
Is authenticated?
↓
Is role Admin?
↓
Execute API
Even if somebody bypasses Angular, ASP.NET Core still enforces authorization.
15. Recommended Architecture
For your Angular + ASP.NET Core application:
User
|
v
Angular
|
Route Guard
|
+--------+--------+
| |
Allowed Blocked
| |
v v
Component Login / 403
|
v
HttpClient
|
v
ASP.NET Core API
|
Authentication
|
Authorization
|
Policy / Role
|
v
Business Operation
|
v
SQL Server
The backend authorization check is authoritative.
16. Authentication vs Authorization Guard
These are conceptually different.
Authentication Guard
Checks:
Is the user logged in?
Example:
if (authService.isLoggedIn()) {
return true;
}
Used for:
/dashboard
/profile
/orders
Authorization Guard
Checks:
Does the authenticated user have permission?
Example:
if (authService.role() === 'Admin') {
return true;
}
Used for:
/admin
/admin/users
/admin/articles
17. Role-Based vs Policy-Based Authorization
For simple applications, roles can work:
[Authorize(Roles = "Admin")]
But larger applications often benefit from policy-based authorization.
For example:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(
"CanManageArticles",
policy =>
{
policy.RequireClaim(
"permission",
"articles.manage");
});
});
Then:
[Authorize(
Policy = "CanManageArticles")]
[HttpDelete("{id}")]
public IActionResult DeleteArticle(
Guid id)
{
// Delete article
return NoContent();
}
This can be more flexible than hard-coding roles everywhere.
18. Handle 401 and 403 Correctly
Your Angular application should distinguish:
401 Unauthorized
from:
403 Forbidden
Despite the HTTP status name, think of 401 as an authentication problem.
401
Not authenticated
or authentication expired
Usually:
401
↓
Login page
403
Authenticated
but insufficient permission
Usually:
403
↓
Unauthorized / Access Denied page
So:
API Response
|
+---- 401 ----> Login
|
+---- 403 ----> Access Denied
19. Use an HTTP Error Interceptor
In your real Angular application, you can globally handle these responses.
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
);
}
)
);
};
Then register it:
provideHttpClient(
withInterceptors([
errorInterceptor
])
)
The route guard and interceptor have different purposes:
Route Guard
↓
Prevent inappropriate navigation
Error Interceptor
↓
React to authorization failures
returned by the API
20. What About CanMatch?
For sensitive lazy-loaded routes, CanMatch can also be useful.
Example:
{
path: 'admin',
canMatch: [adminGuard],
loadComponent: () =>
import('./admin')
.then(m => m.Admin)
}
CanMatch determines whether a route configuration can be matched.
This can be useful when you don't want an unauthorized user to match a particular route.
But again:
CanMatchis still client-side behavior, not a substitute for ASP.NET Core authorization.
21. Hiding an Admin Button Is Not Security
Suppose you write:
@if (authService.role() === 'Admin') {
<button>
Delete User
</button>
}
That's useful for UI/UX.
But it doesn't secure:
DELETE /api/users/10
The server still needs something such as:
[Authorize(Roles = "Admin")]
Think:
Hide button
=
UX
Route Guard
=
Navigation control
[Authorize]
=
Security
22. Key Points
1. Use Angular route guards
For example:
canActivate: [authGuard]
2. Use authorization guards for roles/permissions
For example:
canActivate: [
authGuard,
adminGuard
]
3. Never rely on Angular guards for security
Angular runs in the user's browser and can be bypassed.
4. Secure the ASP.NET Core API
Use:
[Authorize]
or:
[Authorize(Roles = "Admin")]
or preferably policies where they fit:
[Authorize(
Policy = "CanManageArticles")]
5. Handle 401 and 403 differently
401 → Authentication issue → Login
403 → Authorization issue → Access Denied
6. Don't treat hidden UI elements as security
Hiding buttons or menus improves UX but doesn't protect the underlying API.
23. Interview Questions and Answers
1. How do you secure sensitive routes in Angular?
Answer:
Use Angular route guards such as CanActivate or CanMatch to control client-side navigation. However, the corresponding ASP.NET Core endpoints must also enforce authorization using [Authorize], roles, claims, or policies.
2. Is an Angular route guard enough for security?
No.
Angular runs on the client and can be bypassed. The backend must always enforce authorization independently.
3. What is the difference between authentication and authorization guards?
Authentication checks whether the user is logged in.
Authorization checks whether the authenticated user has the required role or permission.
4. What should happen on 401?
A 401 indicates that valid authentication is missing or no longer accepted.
Angular will commonly redirect the user to:
/login
5. What should happen on 403?
A 403 means the user is authenticated but doesn't have sufficient permission.
Angular can redirect to:
/unauthorized
6. Should we hide unauthorized menu items?
Yes, for a better user experience.
But hiding:
Admin
Delete
Edit
buttons does not provide security. The corresponding APIs must still enforce authorization.
7. CanActivate vs [Authorize]?
CanActivate
↓
Angular
↓
Client-side navigation
[Authorize]
↓
ASP.NET Core
↓
Server-side security
[Authorize] is the authoritative security control.
Interview Quick Answer
I secure sensitive routes at both the Angular and ASP.NET Core levels. Angular route guards such as
CanActivateorCanMatchcontrol client-side navigation and improve UX, while ASP.NET Core uses[Authorize], roles, claims, or authorization policies to enforce actual security. I also handle401by directing unauthenticated users to login and403by showing an access-denied page. Angular guards never replace backend authorization.