This episode covers security and authentication: JWT authentication patterns, protecting routes with auth guards, secure token storage and HTTP calls, and role-based authorization and permission checks.

Most applications store user data — and that data must be protected. Authentication verifies who the user is, while authorization determines what they're allowed to access.
Episode 12 covers JWT authentication patterns, protecting routes with auth guards, secure token storage and HTTP calls, and role-based authorization and permission checks. Security isn't an add-on feature — it's part of the architecture.
JWT (JSON Web Token) is a token containing verified claims. The common flow: the user logs in, the server returns an access token (and usually a refresh token), the application stores it, then sends it in the header of every request.
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly http = inject(HttpClient);
readonly pengguna = signal<Pengguna | null>(null);
login(email: string, password: string): void {
this.http.post<LoginResponse>('/api/login', { email, password })
.subscribe({
next: (res) => {
this.simpanToken(res.accessToken);
this.pengguna.set(res.pengguna);
},
error: (err) => console.error('Login gagal', err),
});
}
}login sends the credentials, then stores the access token and the user state. After this, the application knows the user is authenticated and can show the appropriate UI.
Token storage determines the level of security:
The modern best practice: keep the access token in memory and the refresh token in an httpOnly cookie. This avoids XSS while preserving the session.
The guard from episode 9 is used to protect routes that require login:
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.pengguna() !== null) {
return true;
}
return router.createUrlTree(['/login'], { queryParams: { kembali: router.url } });
};Attach the guard to a protected route: { path: 'profil', component: ProfilComponent, canActivate: [authGuard] }. When a user who isn't logged in tries to open that route, they're redirected to the login page with a note of the original page.
Sometimes the token exists, but the user data hasn't been loaded yet. authGuard can wait for the profile update to finish:
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
const status = auth.muatProfil();
return status ? true : router.createUrlTree(['/login']);
};Here muatProfil checks the local token and loads the user data if needed. The guard only returns true once the authentication state is truly certain.
Every API request must carry the token. The centralized place for this is an HTTP interceptor:
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.getToken();
if (!token) {
return next(req);
}
const reqBaru = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
return next(reqBaru);
};authInterceptor adds the Authorization: Bearer <token> header to every request. The interceptor is registered in app.config.ts via provideHttpClient(withInterceptors([authInterceptor])). All requests are authenticated automatically without changing code in every service.
When an access token expires, the server returns status 401. Handle it centrally: refresh the token via an endpoint, retry the failed request, or redirect the user to the login page. This pattern keeps the session alive without forcing users to log in repeatedly.
Role-based access control (RBAC) checks a user's role before granting access:
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const roleGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.pengguna()?.role === 'admin') {
return true;
}
return router.createUrlTree(['/forbidden']);
};roleGuard only allows users with the admin role. The forbidden page shows an access-denied message. Combine auth and role guards for areas that need both.
Don't just hide admin buttons in the UI — permissions must also be verified in the backend. The UI hides or disables actions the user isn't allowed to perform, while the backend still validates every request. These two layers protect the application from direct manipulation through the API.
Key takeaways:
In the next episode, episode 13, we'll cover secure web practices — preventing XSS, CSRF, and injection, applying content security policy and secure headers, sanitizing HTML and handling safe URLs, and client-side security best practices.