Skip to content

Commit

Permalink
Added setup of CSRF, and interceptor that adds/gets relevant token he…
Browse files Browse the repository at this point in the history
…ader. WithCredentials is needed to send/receive cookies.
  • Loading branch information
MadsApollo committed Sep 23, 2024
1 parent ec9810c commit 1323682
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 1 deletion.
10 changes: 9 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BrowserModule, Title } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { TranslateModule, TranslateLoader } from "@ngx-translate/core";
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http";
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS, HttpClientXsrfModule } from "@angular/common/http";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { NavbarModule } from "./navbar/navbar.module";
Expand Down Expand Up @@ -31,6 +31,8 @@ import { UserPageComponent } from "./admin/users/user-page/user-page.component";
import { SharedModule } from "@shared/shared.module";
import { PipesModule } from "@shared/pipes/pipes.module";
import { CookieService } from "ngx-cookie-service";
import { CsrfInterceptor } from "@shared/helpers/csrf-interceptor";
import { CsrfCookieName, CsrfHeaderName } from "@shared/constants/csrf-constants";

export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, "./assets/i18n/", ".json");
Expand Down Expand Up @@ -79,6 +81,11 @@ export function tokenGetter() {
MonacoEditorModule.forRoot(),
WelcomeDialogModule,
PipesModule,
HttpClientXsrfModule,
HttpClientXsrfModule.withOptions({
cookieName: CsrfCookieName, // Match the cookie name used by the backend
headerName: CsrfHeaderName, // Match the header name expected by the backend
}),
],
bootstrap: [AppComponent],
exports: [TranslateModule],
Expand All @@ -87,6 +94,7 @@ export function tokenGetter() {
//{ provide: ErrorHandler, useClass: GlobalErrorHandler },
//{ provide: HTTP_INTERCEPTORS, useClass: ServerErrorInterceptor, multi: true },
Title,
{ provide: HTTP_INTERCEPTORS, useClass: CsrfInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: AuthJwtInterceptor, multi: true },
{ provide: SAVER, useFactory: getSaver },
{ provide: MatPaginatorIntl, useClass: MatPaginatorIntlDa },
Expand Down
2 changes: 2 additions & 0 deletions src/app/shared/constants/csrf-constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const CsrfCookieName = "token-cookie-name";
export const CsrfHeaderName = "x-csrf-token";
45 changes: 45 additions & 0 deletions src/app/shared/helpers/csrf-interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { HttpClient, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { environment } from "@environments/environment";
import { CsrfHeaderName } from "@shared/constants/csrf-constants";
import { Observable, of } from "rxjs";
import { catchError, switchMap, tap } from "rxjs/operators";

@Injectable()
export class CsrfInterceptor implements HttpInterceptor {
private baseUrl = environment.baseUrl;
private tokenUrl = "csrf/token";
private csrfToken: string;
constructor(private httpClient: HttpClient) {}

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.csrfToken || req.url.endsWith(this.tokenUrl)) {
const cloned = req.clone({
headers: req.headers.set(CsrfHeaderName, this?.csrfToken ?? ""),
withCredentials: req.withCredentials || ["POST", "PUT", "DELETE"].includes(req.method),
});

return next.handle(cloned);
}

// If no CSRF token, fetch it first
return this.fetchCsrfToken().pipe(
switchMap(() => {
return next.handle(req);
}),
catchError(err => {
console.error("CSRF token fetch failed", err);
return next.handle(req);
})
);
}

private fetchCsrfToken(): Observable<string> {
return this.httpClient.get<{ token: string }>(this.baseUrl + this.tokenUrl, { withCredentials: true }).pipe(
tap(response => {
this.csrfToken = response.token;
}),
switchMap(response => of(response.token))
);
}
}

0 comments on commit 1323682

Please sign in to comment.