Skip to content

Commit

Permalink
Support CSRF protection
Browse files Browse the repository at this point in the history
  • Loading branch information
cjmalloy committed Nov 27, 2023
1 parent bde083c commit 5ea057c
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 12 deletions.
11 changes: 0 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,6 @@ docker compose file.

## Features

## CSRF Protection
All modifying or sensitive server endpoints in this project must not use GET as a method in order to prevent cross site
request forgeries. All pages, upon load, with no user interaction, should not make any modifying or sensitive request.
No combination of url path, parameters, or hash should cause such a request. CSRF attacks require that the app perform
some action simply by loading a page.

By preventing CSRF on the client side, we prevent the need to copy CSRF tokens into headers and can persist auth tokens
in secure cookies. CSRF-TOKEN headers have issues where adding headers is difficult, such as loading images or
downloading files in a new tab. By storing auth tokens in a secure cookie, we can allow the user to open a new tab and
still be signed in.

### Text Editor
Markdown editor with support for rendering both markdown and HTML.
* Use `#tag` to create a tag link `[tag](/tag/politics)` (add a space for the usual markdown
Expand Down
4 changes: 3 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DragDropModule } from '@angular/cdk/drag-drop';
import { FullscreenOverlayContainer, OverlayContainer, OverlayModule } from '@angular/cdk/overlay';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { HttpClientModule } from '@angular/common/http';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, HAMMER_GESTURE_CONFIG, HammerModule } from '@angular/platform-browser';
Expand Down Expand Up @@ -101,6 +101,7 @@ import { ThemesFormComponent } from './form/themes/themes.component';
import { UserFormComponent } from './form/user/user.component';
import { JasperFormlyModule } from './formly/formly.module';
import { HammerConfig } from './hammer.config';
import { CsrfInterceptor } from './http/csrf.interceptor';
import { ExtPage } from './page/ext/ext.component';
import { HomePage } from './page/home/home.component';
import { InboxAlarmsPage } from './page/inbox/alarms/alarms.component';
Expand Down Expand Up @@ -324,6 +325,7 @@ const loadFactory = (config: ConfigService, debug: DebugService, authn: AuthnSer
})
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: CsrfInterceptor, multi: true },
{ provide: OverlayContainer, useClass: FullscreenOverlayContainer },
{ provide: HAMMER_GESTURE_CONFIG, useClass: HammerConfig },
{
Expand Down
20 changes: 20 additions & 0 deletions src/app/http/csrf.interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';

import { CsrfInterceptor } from './csrf.interceptor';

describe('CsrfInterceptor', () => {
beforeEach(() => TestBed.configureTestingModule({
providers: [
CsrfInterceptor
],
imports: [
HttpClientTestingModule,
]
}));

it('should be created', () => {
const interceptor: CsrfInterceptor = TestBed.inject(CsrfInterceptor);
expect(interceptor).toBeTruthy();
});
});
30 changes: 30 additions & 0 deletions src/app/http/csrf.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable, isDevMode } from '@angular/core';
import { Observable } from 'rxjs';
import { ConfigService } from '../service/config.service';

@Injectable()
export class CsrfInterceptor implements HttpInterceptor {

withCredentials = isDevMode() || this.config.electron;

constructor(
private config: ConfigService,
) {}

private getCsrfToken(): string {
return document.cookie.split('; ').find(row => row.startsWith('XSRF-TOKEN='))?.split('=')?.[1] || '';
}

intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
if (request.method === 'GET' || request.method === 'HEAD' || request.method === 'OPTIONS') {
return next.handle(request);
}

const modifiedReq = request.clone({
headers: request.headers.set('X-XSRF-TOKEN', this.getCsrfToken()),
withCredentials: this.withCredentials,
});
return next.handle(modifiedReq);
}
}

0 comments on commit 5ea057c

Please sign in to comment.