Access control
This section outlines recommended approaches for implementing authentication and authorization on the front-end with Angular, in a secure and maintainable way.
Authentication is the process of verifying the identity of a user, using credentials such as username and password, Single Sign-On (SSO) or other methods.
Authorization is the process of determining what actions a user is allowed to perform and what resources he can access.
Authentication
Do choose a well-established authentication protocol and authentication flow.
This guide will not give recommendations on what protocol or authentication flow to use because it highly depends on your specific use case, security requirements, architecture, identity provider, infrastructure, etc.
Avoid implementing your own authentication system.
Implementing custom authentication is very complex and error-prone, it will most likely lead to security vulnerabilities if you are not a security expert.
Do use a client library to handle authentication flow.
Consider using a service to globally manage authentication and interact with the chosen client library.
@Injectable({ providedIn: 'root' })
export class AuthManager {
// Expose readonly signals
isAuthenticated: Signal<boolean> = ...;
userInfo: Signal<UserInfo | null> = ...;
// Implementation details depends on your authentication flow, protocol and client library.
login() {...}
logout() {...}
}
Avoid storing sensitive information (e.g. token) in local storage or session storage.
Local and session storage are vulnerable to cross-site scripting (XSS) attacks.
Token management
Most modern authentication flows are token-based, i.e. a piece of data is exchanged between the server and the client to verify a user's identity and permissions, and the process generally looks like this from the client's perspective:
- Issuance: a token is generated by the server after identity check (e.g. using credentials) and then sent to the client.
- Presentation: the client includes the generated token in its subsequent requests to access protected resources or perform actions that require authentication and authorization. The server checks the token validity and the user's permissions for each request.
- Renewal: the client can request a new token from the server for seamless user experience, before or after expiration.
- Revocation: the client can log out to invalidate the token, or the server can revoke the token for security reasons (e.g. user's password changed).
How tokens are exchanged between the server and the client, whether it's using a cookie or a header, will impact token management and security in your Angular application.
Using a cookie
Consider using an HttpOnly
cookie to store the authentication token.
HttpOnly cookies are created by the server and are not accessible on the client via JavaScript, which helps mitigate the risk of cross-site scripting (XSS) attacks.
Cookies are automatically included by the browser in every HTTP request sent to the server if it has the same origin, no need to manually add them to each request header.
Cookies are also compatible with native browser file downloads, i.e. <img src="...">
and <a download href="...">
tags, which make it easier to download files protected by authentication.
Consider setting SameSite=Strict
to the authentication cookie.
Setting SameSite=Strict
on cookies helps prevent cross-site request forgery (CSRF) attacks by ensuring that cookies are only sent in requests originating from the same site.
Do set withCredentials: true
in your HTTP requests to support authenticated cross-origin requests.
export const credentialsInterceptor: HttpInterceptorFn = (req, next) => {
// Do NOT share credentials to third-party APIs or public resources.
if (!req.url.startsWith(environment.apiBaseUrl)) {
return next(req);
}
// Clone the request to add the 'withCredentials' property.
const requestWithCredentials = req.clone({
withCredentials: true
});
return next(requestWithCredentials);
};
By default, cookies are not sent with cross-origin requests, i.e. if your API is hosted on a different domain than your Angular application.
Using a header
Prefer using a cookie for authentication, but if you need to use an HTTP header, follow the guidelines below.
Do use an interceptor to add HTTP request headers.
export const authTokenInterceptor: HttpInterceptorFn = (req, next) => {
// Do NOT send token to third-party APIs or public resources.
if (!req.url.startsWith(environment.apiBaseUrl)) {
return next(req);
}
// Do nothing if the user is not authenticated.
const authManager = inject(AuthManager);
if (!authManager.token) {
return next(req);
}
// Clone the request to add the 'Authorization' header.
const authenticatedRequest = req.clone({
setHeaders: {
Authorization: `Bearer ${authManager.token}`
}
});
return next(authenticatedRequest);
};
HTTP headers are not propagated automatically by the browser, so you need to manually add them to each request. Using an interceptor allows you to add the authentication token to every HTTP request made with Angular's HttpClient
, centralizing authentication logic in one place.
Protecting routes
Do use guards to restrict access to routes to authenticated users.
export const authenticatedGuard: CanMatchFn = () => {
const authManager = inject(AuthManager);
return authManager.isAuthenticated();
};
const appRoutes: Routes = [
{
path: 'profile',
component: UserProfilePage,
canMatch: [authenticatedGuard]
}
];
Consider using a guard to redirect unauthenticated users to the login page.
export const authenticatedGuard: CanMatchFn = () => {
const authManager = inject(AuthManager);
if (!authManager.isAuthenticated()) {
const router = inject(Router);
return new RedirectCommand(router.parseUrl('/login'));
}
return true;
};
Protecting components
Consider using a structure directive to conditionally render UI elements based on authentication status.
<a routerLink="/profile" *isAuthenticated>Profile</a>
@Directive({
selector: '[isAuthenticated]'
})
export class IsAuthenticated implements OnInit {
#template = inject(TemplateRef);
#viewContainer = inject(ViewContainerRef);
#authManager = inject(AuthManager);
ngOnInit(): void {
if (this.#authManager.isAuthenticated()) {
this.#viewContainer.createEmbeddedView(this.#template);
}
}
}
Libraries
Consider using one of the following:
Authorization
Do filter data based on user permissions on the server side, not on the client side.
Relying on client-side filtering can expose sensitive data to unauthorized users. An attacker can easily bypass client-side checks, inspect network requests or directly request server data using tools like Postman or cURL.
Consider using a service to globally manage user permissions.
export type UserPermission = 'read_post' | 'write_post' | 'write_comment' | 'read_comment';
@Injectable({ providedIn: 'root' })
export class PermissionManager {
#authManager = inject(AuthManager);
// Use a 'Set' for efficient lookup.
#permissions = computed(() => {
return new Set(this.#authManager.userInfo()?.permissions ?? []);
});
hasPermission(permission: UserPermission): boolean {
return this.#permissions().has(permission);
}
hasAnyPermission(permissions: UserPermission[]): boolean {
return permissions.some(permission => this.hasPermission(permission));
}
hasEveryPermission(permissions: UserPermission[]): boolean {
return permissions.every(permission => this.hasPermission(permission));
}
}
Protecting routes
Do use guards to protect routes based on user permissions.
export const permissionGuard: (permission: UserPermission) => CanMatchFn = (permission) => {
return () => inject(PermissionManager).hasPermission(permission);
};
const appRoutes: Routes = [
{
path: 'post/:id',
component: PostPage,
canMatch: [permissionGuard('read_post')]
resolve: {
post: postResolver
}
}
];
If you need to restrict access to a specific resource (e.g. a specific post), you should check permissions on the server side and return an appropriate error response (e.g. 403 Forbidden or 404 Not Found) that can be handled by a resolver on the client side.
Protecting components
Consider using a structure directive to conditionally render UI elements based on user permissions.
<button *hasPermission="'write_post'">Create Post</button>
@Directive({
selector: '[hasPermission]'
})
export class HasPermission implements OnInit {
#template = inject(TemplateRef);
#viewContainer = inject(ViewContainerRef);
#permissionManager = inject(PermissionManager);
hasPermission = input.required<UserPermission>();
ngOnInit(): void {
if (this.#permissionManager.hasPermission(this.hasPermission())) {
this.#viewContainer.createEmbeddedView(this.#template);
}
}
}