Angular Route Guards for Auth & Permissions

Blog / Angular · December 29, 2018 · Updated June 10, 2026 · 9 min read
Angular Route Guards for Auth & Permissions

Angular route guards are functions (or, in legacy code, classes) that the Router runs before it activates, loads, or leaves a route, returning true, false, or a UrlTree to decide whether navigation is allowed. They are how you protect client-side routes for authentication, role and permission checks, lazy-loaded sections, and unsaved-change prompts.

This guide shows the modern, 2026 way to build them in Angular v17, v18, and v19: functional guards with inject() and standalone APIs. If you learned Angular in the v2–v4 era you wrote class-based guards that implemented the CanActivate interface — those still run, but they have been deprecated since Angular 15.2 and the framework now steers everyone toward small, composable guard functions.

Key takeaways

  • Route guards decide whether the Router lets a user enter, leave, load, or match a route — they are a UX gate, not a security boundary.
  • Functional guards (CanActivateFn, CanMatchFn, etc.) are the current best practice; class-based guards are deprecated since Angular 15.2.
  • Use inject() to pull AuthService and Router into a guard — no constructor needed.
  • A guard can redirect by returning a UrlTree (or a RedirectCommand), not just block with false.
  • Read role/permission requirements from the route's data property via ActivatedRouteSnapshot.
  • CanMatch replaced the deprecated CanLoad for guarding lazy-loaded routes.
  • Critically: guards run in the browser for experience only — real authorization must be enforced on your server/API.

What are Angular route guards?

Angular route guards are hooks in the Router lifecycle. When a user clicks a link or you call router.navigate(), the Router collects every guard attached to the matched route and runs them before it commits the navigation. Each guard returns one of:

  • true — allow the navigation to continue.
  • false — cancel the navigation.
  • a UrlTree — cancel and redirect to a different route (for example, send an anonymous user to /login).
  • a RedirectCommand (Angular 19+) — redirect with extra navigation options such as replaceUrl.
  • an Observable or Promise of any of the above — for async checks like calling a token-refresh endpoint.

The guard's return type is summarised by Angular as GuardResult = boolean | UrlTree | RedirectCommand. Because a guard can return an async value, you can wait on a real API call before deciding — just remember the decision still happens in the browser.

What are the guard types in modern Angular?

There are five guard kinds, each with a functional type. Pick the one that matches when you need to make the decision.

Guard Functional type Runs when Use it to
CanActivate CanActivateFn Before a route is entered Block a route behind auth or a role
CanActivateChild CanActivateChildFn Before any child route is entered Protect a whole section from one place
CanDeactivate CanDeactivateFn Before the user leaves a route Warn about unsaved form changes
CanMatch CanMatchFn While the Router matches a route Conditionally match/load a route (replaces CanLoad) and fall through to another route
Resolve ResolveFn After match, before activation Pre-fetch data the component needs to render

CanMatch is the most powerful addition: because it runs during matching, an unauthorised user never downloads the lazy bundle, and the Router can fall through to a catch-all route (such as a login page) instead of erroring.

Functional vs class-based guards: what changed?

Old Angular tutorials (including the original version of this article) defined a guard as an @Injectable() class that implements CanActivate. That pattern was deprecated in Angular 15.2. The modern equivalent is a plain function typed as CanActivateFn that uses inject() to reach its dependencies. Functional guards are smaller, easier to unit test, and trivial to compose — you can list several in one canActivate: [a, b] array.

Aspect Functional guards (current) Class-based guards (legacy)
Status Recommended, default since v14–v15 Deprecated since Angular 15.2
Definition export const guard: CanActivateFn = ... @Injectable() class implements CanActivate
Dependencies inject(Service) Constructor injection
Boilerplate Minimal — just a function Class + decorator + provider
Registration canActivate: [authGuard] canActivate: [AuthGuard]
Testing Call the function with mocks Instantiate the class with a TestBed

If you are still on NgModules, functional guards work there too — the standalone APIs below are optional, not required.

How do you build an auth guard in Angular?

A typical authentication guard checks whether a user is signed in and, if not, redirects to the login page while preserving where they were headed. Here is the functional version using inject(). Assume an AuthService exposes an isLoggedIn() method (often backed by a signal or a token check).

// auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router, UrlTree } from '@angular/router';
import { AuthService } from './auth.service';

// Functional CanActivateFn — the modern replacement for class-based guards.
// inject() pulls services from the DI container; no constructor required.
export const authGuard: CanActivateFn = (route, state): boolean | UrlTree => {
  const auth = inject(AuthService);
  const router = inject(Router);

  if (auth.isLoggedIn()) {
    return true;
  }

  // Cancel navigation and redirect to /login, remembering the target URL.
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url },
  });
};

How do you wire a guard into the routes?

Attach the guard to a route with the canActivate array, then register your routes with provideRouter() in a standalone bootstrap (or RouterModule.forRoot(routes) if you still use NgModules). Note that you reference the function itself — authGuard, not [AuthGuard].

// app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './auth.guard';
import { roleGuard } from './role.guard';
import { DashboardComponent } from './dashboard/dashboard.component';
import { AdminComponent } from './admin/admin.component';

export const routes: Routes = [
  {
    path: 'login',
    loadComponent: () => import('./login/login.component').then(m => m.LoginComponent),
  },
  { path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] },
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [authGuard, roleGuard], // guards run in array order
    data: { roles: ['admin'] },          // read by roleGuard below
  },
];

// main.ts — standalone bootstrap, no NgModule needed.
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [provideRouter(routes)],
});

How do you guard a route by role or permission?

For authorization you usually need more than "is the user logged in?" — you need "does this user have the right role?" Store the required roles on the route's data object and read them inside the guard from the ActivatedRouteSnapshot. This keeps one reusable guard that works for every protected route.

// role.guard.ts
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

// Reads allowed roles from route.data and checks them against the user's roles.
export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  const allowedRoles = (route.data['roles'] as string[]) ?? [];
  const userRoles = auth.getRoles(); // e.g. ['editor', 'admin']

  const hasAccess = allowedRoles.some(role => userRoles.includes(role));

  // Bounce unauthorised users to a safe page instead of just returning false.
  return hasAccess ? true : router.parseUrl('/forbidden');
};

How do you guard a lazy-loaded route?

CanLoad is deprecated — use CanMatchFn instead. Because it runs while the Router is matching routes, an unauthorised user never downloads the protected bundle, and the Router can fall through to the next matching route (a clean way to show a login page for an unknown user).

// admin-area.guard.ts
import { inject } from '@angular/core';
import { CanMatchFn, Route, Router, UrlSegment } from '@angular/router';
import { AuthService } from './auth.service';

// CanMatchFn replaces the deprecated CanLoad. Returning a UrlTree both blocks
// the lazy bundle and redirects.
export const adminAreaGuard: CanMatchFn = (route: Route, segments: UrlSegment[]) => {
  const auth = inject(AuthService);
  const router = inject(Router);
  return auth.isLoggedIn() ? true : router.parseUrl('/login');
};

Are route guards real security?

No. Route guards run entirely in the browser and are a user-experience feature, not a security boundary. Anyone can open dev tools, edit the bundled JavaScript, replay your API calls, or simply hit your endpoints directly with curl — the guard never executes for them. Treat guards as a way to hide UI a user shouldn't see and to redirect cleanly, nothing more.

Every protected action and every piece of sensitive data must be authorised again on the server or API, using a verified session or token and a server-side role/permission check. The client guard and the server check should agree, but the server is the one that actually enforces access. If you only guard on the client, your app is not secured.

Migrating legacy class-based guards

If you have an older codebase, you do not have to rewrite everything at once. Class guards still run, and you can wrap an existing class guard in a functional one so it keeps working while you migrate. For reference, here is the deprecated class style alongside the bridge pattern.

// LEGACY — class-based guard, deprecated since Angular 15.2. Avoid in new code.
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(private router: Router) {}

  canActivate(): boolean {
    if (localStorage.getItem('token')) {
      return true;
    }
    this.router.navigate(['/login']);
    return false;
  }
}

// BRIDGE — reuse an existing class guard from the routes without rewriting it yet.
// canActivate: [() => inject(AuthGuard).canActivate()]

Related Angular guides

If you are building out a full client-side app, these companion articles pair well with route guarding:

Need a production-grade Angular app with authentication, role-based access, and a hardened API behind it? Our team has shipped 50+ projects for startups and enterprises — see our Angular development services to talk through your build.

Frequently Asked Questions

Are Angular route guards enough to secure my application?

No. Angular route guards run client-side and only control what the browser renders and navigates to. They can be bypassed by editing JavaScript or calling your API directly. You must enforce authentication and authorization again on the server or API for every protected request — the guard is for user experience, the server is for security.

What replaced CanLoad in Angular?

CanLoad is deprecated and has been replaced by CanMatch (CanMatchFn). CanMatch runs while the Router matches routes, so it can prevent a lazy-loaded bundle from downloading and let the Router fall through to another matching route, which CanLoad could not do.

Are class-based route guards still supported in 2026?

Class-based guards still execute, but they have been deprecated since Angular 15.2. New code should use functional guards (CanActivateFn, CanMatchFn, and so on) with inject(). You can bridge an existing class guard from a route with () => inject(MyGuard).canActivate() while you migrate.

How do I pass data such as roles to an Angular guard?

Put the values on the route's data property, for example data: { roles: ['admin'] }. Inside a functional guard, read them from the ActivatedRouteSnapshot with route.data['roles']. This lets one reusable guard handle different routes with different role requirements.

Can an Angular guard redirect instead of just blocking?

Yes. Return a UrlTree (built with router.createUrlTree([...]) or router.parseUrl('/path')) and the Router cancels the current navigation and redirects there. In Angular 19+ you can return a RedirectCommand to redirect with extra options such as replaceUrl. Returning false simply cancels with no redirect.

How do I run an async check, like an API call, inside a guard?

Return an Observable or Promise that resolves to boolean or UrlTree. For example, inject your auth service and return auth.checkSession().pipe(map(ok => ok || router.parseUrl('/login'))). The Router waits for the async result before completing the navigation.

Share this article