Authentication and authorization are critical components of modern web applications. Authentication ensures that a user is who they claim to be, while authorization determines what resources the user can access. Proper implementation ensures application security, protects sensitive data, and enhances user trust. In Angular, these processes are typically implemented using JWT (JSON Web Tokens), route guards, HTTP interceptors, and role-based access control (RBAC).
This post covers best practices for secure storage of tokens, handling expiration, protecting routes, enforcing role-based access, backend validation, and testing APIs.
1. Understanding Authentication vs Authorization
- Authentication: Verifying user identity.
- Authorization: Determining what resources or actions the authenticated user can perform.
Example:
// Authentication
if (authService.isAuthenticated()) {
console.log('User is logged in');
}
// Authorization
if (authService.canAccess('admin')) {
console.log('User can access admin dashboard');
}
2. Using JWTs for Stateless Authentication
JWTs are commonly used for authentication due to their stateless and scalable nature.
Tokens are issued by the backend after successful login and are attached to subsequent requests.
Storing JWT in localStorage or sessionStorage:
localStorage.setItem('token', res.token);
const token = localStorage.getItem('token');
Using JWT in HTTP requests:
const headers = { Authorization: Bearer ${token}
};
this.http.get('/api/protected', { headers }).subscribe(res => console.log(res));
Note: Never store sensitive info like passwords in storage without encryption.
3. Never Store Sensitive Info in Plain Storage
- Avoid storing passwords or sensitive data in localStorage or sessionStorage directly.
- Use JWTs with short expiration to minimize security risks.
- Consider encrypting stored data using libraries like
crypto-js
.
Example: Encrypting token before storage:
import * as CryptoJS from 'crypto-js';
const encryptedToken = CryptoJS.AES.encrypt(res.token, 'secret-key').toString();
localStorage.setItem('token', encryptedToken);
// Decrypt token before use
const bytes = CryptoJS.AES.decrypt(localStorage.getItem('token')!, 'secret-key');
const token = bytes.toString(CryptoJS.enc.Utf8);
4. Use JWT Expiration and Refresh Tokens
JWTs should include an expiration time to limit their validity.
Example: Decoding and checking token expiration
import jwtDecode from 'jwt-decode';
const token = localStorage.getItem('token');
if (token) {
const decoded: any = jwtDecode(token);
const isExpired = Date.now() > decoded.exp * 1000;
if (isExpired) {
authService.refreshToken();
}
}
Refresh token workflow:
- Issue a short-lived JWT and a long-lived refresh token.
- Use refresh token to get a new JWT when expired.
- Store refresh token securely in HTTP-only cookies.
5. Frontend Guards for Route Protection
Angular route guards protect routes based on authentication and authorization.
AuthGuard Example:
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router) {}
canActivate(): boolean {
if (this.auth.isAuthenticated()) {
return true;
}
this.router.navigate(['/login']);
return false;
}
}
Role-based guard example:
@Injectable({ providedIn: 'root' })
export class RoleGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot): boolean {
const requiredRole = route.data['role'];
if (this.auth.canAccess(requiredRole)) {
return true;
}
this.router.navigate(['/unauthorized']);
return false;
}
}
Apply in routes:
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [AuthGuard, RoleGuard],
data: { role: 'admin' }
}
];
6. Backend Validation
Frontend guards alone are not sufficient. Always validate JWTs and permissions on the server.
Node.js Example:
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
Role-based backend check:
function authorizeRole(role) {
return (req, res, next) => {
if (req.user.role !== role) {
return res.sendStatus(403);
}
next();
};
}
7. Encrypting Tokens and Sensitive Data
- Use libraries like
crypto-js
for encrypting tokens in localStorage. - Use HTTP-only cookies for storing refresh tokens (prevents XSS attacks).
Example storing encrypted refresh token in cookie:
document.cookie = refreshToken=${encryptedToken}; HttpOnly; Secure; SameSite=Strict
;
8. HTTP Interceptors for Automatic Token Attachment
Avoid manually adding tokens to every request.
JWT Interceptor:
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler) {
const token = localStorage.getItem('token');
const cloned = token ? req.clone({ setHeaders: { Authorization: Bearer ${token}
} }) : req;
return next.handle(cloned);
}
}
Register in AppModule:
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }
]
9. Logging Out and Token Revocation
Always clear tokens on logout:
logout() {
localStorage.removeItem('token');
sessionStorage.removeItem('token');
this.router.navigate(['/login']);
}
Optionally, notify the backend to revoke the refresh token.
10. Role-Based Access Control (RBAC)
Roles determine what parts of an application a user can access.
AuthService Role Methods:
getUserRole(): string | null {
const token = localStorage.getItem('token');
if (!token) return null;
const payload: any = jwtDecode(token);
return payload.role;
}
canAccess(requiredRole: string | string[]): boolean {
const userRole = this.getUserRole();
if (!userRole) return false;
if (Array.isArray(requiredRole)) return requiredRole.includes(userRole);
return userRole === requiredRole;
}
11. Securing Sensitive Routes
Combine authentication and role-based guards:
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [AuthGuard, RoleGuard],
data: { role: 'admin' }
},
{
path: 'editor',
component: EditorComponent,
canActivate: [AuthGuard, RoleGuard],
data: { role: ['admin', 'editor'] }
}
];
12. Testing API Protection Thoroughly
- Write unit tests for guards:
it('should block unauthenticated users', () => {
const guard = TestBed.inject(AuthGuard);
spyOn(authService, 'isAuthenticated').and.returnValue(false);
expect(guard.canActivate()).toBeFalse();
});
- Test backend validation by sending requests without tokens, with expired tokens, and with insufficient roles.
- Ensure refresh token logic works for token renewal.
13. Handling Token Expiration Gracefully
Implement automatic logout or token refresh:
import { timer } from 'rxjs';
checkTokenExpiration() {
const token = localStorage.getItem('token');
if (!token) return;
const decoded: any = jwtDecode(token);
const expiresIn = decoded.exp * 1000 - Date.now();
timer(expiresIn).subscribe(() => {
this.refreshToken(); // or logout
});
}
14. Using Refresh Tokens
- Short-lived access token + long-lived refresh token.
- Refresh token can be stored in HTTP-only cookies.
- Avoid exposing refresh token to client-side scripts.
Refresh flow:
refreshToken() {
return this.http.post('/api/refresh', {}, { withCredentials: true })
.subscribe((res: any) => localStorage.setItem('token', res.token));
}
15. Protecting Against Common Threats
- XSS: Avoid storing sensitive info in localStorage without encryption.
- CSRF: Use HTTP-only cookies for refresh tokens.
- Token theft: Use short expiration and refresh tokens.
16. Example Full AuthService
@Injectable({ providedIn: 'root' })
export class AuthService {
constructor(private http: HttpClient, private router: Router) {}
login(credentials: any) {
return this.http.post('/api/login', credentials).subscribe((res: any) => {
localStorage.setItem('token', res.token);
});
}
logout() {
localStorage.removeItem('token');
this.router.navigate(['/login']);
}
isAuthenticated(): boolean {
const token = localStorage.getItem('token');
return !!token;
}
getUserRole(): string | null {
const token = localStorage.getItem('token');
if (!token) return null;
const payload: any = jwtDecode(token);
return payload.role;
}
canAccess(requiredRole: string | string[]): boolean {
const userRole = this.getUserRole();
if (!userRole) return false;
if (Array.isArray(requiredRole)) return requiredRole.includes(userRole);
return userRole === requiredRole;
}
}
17. Best Practices Summary
- Never store sensitive info in plain localStorage; encrypt if necessary.
- Use JWT expiration and implement refresh tokens.
- Combine frontend guards with backend validation.
- Implement role-based access control for sensitive routes.
- Always test API protection thoroughly.
- Use HTTPS and HTTP-only cookies to protect tokens.
- Log out users automatically when the token expires.
- Avoid exposing refresh tokens to client-side scripts.
- Encrypt tokens if localStorage must be used.
- Maintain clean code structure for AuthService, guards, and interceptors.
Leave a Reply