This is the final project in the training program Web Security Academy. It represents a real-life use case of personal money tracker. There are five main feature modules: Auth, Dashboard, Expenses, Settings and Admin with many security measures implemented described below. The project implements role-based access control (RBAC), giving different users different permissions. This is the frontend part to the accompanying backend api in Node.js with TypeScript. A deployed version (with mocked in-memory API) is available under https://budget.dev-academy.com.
npm install
ng serve
There are two ways you can run the Angular part:
- As a standalone Angular application with
ng serve
- From node project with
ng build --watch
- Angular distribution will be avaiable in
dist/
- Angular and Node application must reside in the some folder
- Angular application must reside in
angular
filder - Node application will look for Angular build in
/angular/dist/
thanks toapp.use(express.static('../angular/dist/'));
- Angular distribution will be avaiable in
Role | Permission |
---|---|
Reader | Read expenses and categories for the account. |
Owner | Create, read, update and delete expenses, categories. Create and delete account's users. |
Admin | Read and delete active users' sessions. |
- Each user belongs to one account.
- Each account may have multiple users.
- Each expense belongs to one account.
- Each account may have multiple expenses.
There are two authentication mechanism implemented in both Angular and Node.js parts:
- Session cookies
- JWT Tokens
In Angular part you can select one mechanism with config setting in src/app/core/config.ts
as presented below.
Remember that both Angular and Node.js selections must match!
interface Config {
[key: string]: string;
auth: 'session' | 'token';
}
export const config: Config = {
auth: 'token' // type-safe
};
You can run the Angular application with completly mocked backend API. To do this you need to import and initalize HttpClientInMemoryWebApiModule
in CoreModule
as in the code snippet below. Make sure it is imported after HttpClientModule
.
Once initalized, all the HTTP request will return mocked data thanks to mock.api.ts
and src/app/mocks/*
. The application deployed to budget.angular-academy.com is using that mocked data. Note, that not all of the data associations are reflected with mocks (as with the real backend API), but most of the features are available.
import { HTTP_INTERCEPTORS, HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { MockApi } from './../mocks/mock.api'; // mocks are defined here ๐
@NgModule({
imports: [
HttpClientModule,
HttpClientInMemoryWebApiModule.forRoot(MockApi, { passThruUnknownUrl: true, delay: 1000 }),
],
declarations: [...],
providers: [...],
exports: [...]
})
export class CoreModule { }
You can find mock users in in-memory-user.repository.ts in the backend API for this application.
NOTE: The security measures on the frontend part are not enought to protect the user. Make sure to check the description of security measures in the backend application in Node repository.
In index.html
there are CSP directives defined to prevent unintended code from execution (Cross-site scripting). Unfortunatelly Angular injects inline styles in the build and 'unsafe-inline'
is a must for style-src
. Note, that CSP reporting is not possible to be setup with meta
tags.
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
connect-src 'self';
font-src data:;
style-src 'self' 'unsafe-inline'">
Built-in mechanism of Angular's '@angular/common/http'
library is used to receive and send anti-xsrf tokens. This requires proper handling on the backend part.
import { HTTP_INTERCEPTORS, HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';
@NgModule({
imports: [
HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: 'XSRF-Token',
headerName: 'XSRF-Token',
}),
],
declarations: [...],
providers: [..],
exports: [..]
})
export class CoreModule { }
Particular components are visible to the users with specified roles. For example a reader doesn't see a button to create a new expense. It's possible thanks to custom structural directive forRoles
(only owner sees the button).
<button mat-fab color="primary" (click)="openExpenseDialog()" *forRoles="['owner']">+</button>
Directive implementation:
import { Directive, Input, ViewContainerRef, TemplateRef } from '@angular/core';
import { AuthService } from '../services/auth.service';
@Directive({
selector: '[forRoles]'
})
export class ForRolesDirective {
roles: string[];
@Input()
set forRoles(roles: string[]|string) {
if (roles != null) {
this.roles = Array.isArray(roles) ? roles : [roles];
this.roles = this.roles.map(r => r.toUpperCase());
} else {
this.roles = [];
}
this.authService.getUserRole$().subscribe(
role => {
if (role && !this.roles.includes(role.toUpperCase())) {
this.viewContainer.clear();
} else {
this.viewContainer.createEmbeddedView(this.templateRef);
}
}
);
}
constructor(
private viewContainer: ViewContainerRef,
private templateRef: TemplateRef<any>,
private authService: AuthService) { }
}
GPL-3.0