diff --git a/packages/stark-core/src/modules.ts b/packages/stark-core/src/modules.ts
index 60fb77d6a7..960aed30c2 100644
--- a/packages/stark-core/src/modules.ts
+++ b/packages/stark-core/src/modules.ts
@@ -4,3 +4,4 @@ export * from "./modules/routing";
export * from "./modules/session";
export * from "./modules/settings";
export * from "./modules/user";
+export * from "./modules/xsrf";
diff --git a/packages/stark-core/src/modules/http/services/http.service.ts b/packages/stark-core/src/modules/http/services/http.service.ts
index f7ade8789c..0d8b2fd4fe 100644
--- a/packages/stark-core/src/modules/http/services/http.service.ts
+++ b/packages/stark-core/src/modules/http/services/http.service.ts
@@ -1,8 +1,4 @@
/* tslint:disable:completed-docs*/
-/**
- * @ignore
- */
-const _cloneDeep: Function = require("lodash/cloneDeep");
import { Deserialize, Serialize } from "cerialize";
import { Observable, throwError, timer } from "rxjs";
// FIXME Adapt mergeMap code --> See: https://github.com/ReactiveX/rxjs/blob/master/MIGRATION.md#howto-result-selector-migration
@@ -28,6 +24,11 @@ import {
import { STARK_LOGGING_SERVICE, StarkLoggingService } from "../../logging/services";
import { STARK_SESSION_SERVICE, StarkSessionService } from "../../session/services";
+/**
+ * @ignore
+ */
+const _cloneDeep: Function = require("lodash/cloneDeep");
+
/**
* @ignore
* Service to make HTTP calls in compliance with the guidelines from the NBB REST API Design Guide.
@@ -40,7 +41,9 @@ export class StarkHttpServiceImpl
implements StarkHttpS
@Inject(STARK_LOGGING_SERVICE) private logger: StarkLoggingService,
@Inject(STARK_SESSION_SERVICE) private sessionService: StarkSessionService,
private httpClient: HttpClient
- ) {}
+ ) {
+ this.logger.debug(starkHttpServiceName + " loaded");
+ }
public executeSingleItemRequest(request: StarkHttpRequest
): Observable> {
// remove the etag before executing the request
diff --git a/packages/stark-core/src/modules/logging/services/logging.service.spec.ts b/packages/stark-core/src/modules/logging/services/logging.service.spec.ts
index 04ebfee48c..e7472d33db 100644
--- a/packages/stark-core/src/modules/logging/services/logging.service.spec.ts
+++ b/packages/stark-core/src/modules/logging/services/logging.service.spec.ts
@@ -1,6 +1,7 @@
/*tslint:disable:completed-docs*/
import Spy = jasmine.Spy;
import SpyObj = jasmine.SpyObj;
+import { Injector } from "@angular/core";
import { Store } from "@ngrx/store";
import { Observable, of, throwError } from "rxjs";
import { Serialize } from "cerialize";
@@ -10,15 +11,17 @@ import { StarkLoggingServiceImpl } from "./logging.service";
import { StarkApplicationConfig, StarkApplicationConfigImpl } from "../../../configuration/entities/application";
import { StarkLogging, StarkLoggingImpl, StarkLogMessage, StarkLogMessageImpl, StarkLogMessageType } from "../../logging/entities";
import { StarkBackend } from "../../http/entities/backend";
+import { StarkXSRFService } from "../../xsrf/services";
import { StarkCoreApplicationState } from "../../../common/store";
import { StarkError, StarkErrorImpl } from "../../../common/error";
+import { MockStarkXsrfService } from "../../xsrf/testing/xsrf.mock";
// tslint:disable-next-line:no-big-function
describe("Service: StarkLoggingService", () => {
let appConfig: StarkApplicationConfig;
let mockStore: SpyObj>;
- // FIXME: uncomment when XSRF service is implemented
- // let mockXSRFService: StarkXSRFService;
+ let mockInjectorService: SpyObj;
+ let mockXSRFService: StarkXSRFService;
let loggingService: LoggingServiceHelper;
const loggingBackend: StarkBackend = {
name: "logging",
@@ -39,6 +42,7 @@ describe("Service: StarkLoggingService", () => {
beforeEach(() => {
mockStore = jasmine.createSpyObj>("store", ["dispatch", "pipe"]);
+ mockInjectorService = jasmine.createSpyObj("injector,", ["get"]);
appConfig = new StarkApplicationConfigImpl();
appConfig.debugLoggingEnabled = true;
appConfig.loggingFlushDisabled = false;
@@ -46,15 +50,16 @@ describe("Service: StarkLoggingService", () => {
appConfig.loggingFlushPersistSize = loggingFlushPersistSize;
appConfig.addBackend(loggingBackend);
- // FIXME: uncomment when XSRF service is implemented
- // mockXSRFService = UnitTestingUtils.getMockedXSRFService();
+ mockXSRFService = new MockStarkXsrfService();
mockStarkLogging = {
uuid: "dummy uuid",
applicationId: "dummy app id",
messages: []
};
mockStore.pipe.and.returnValue(of(mockStarkLogging));
- loggingService = new LoggingServiceHelper(mockStore, appConfig /*, mockXSRFService*/);
+ /* tslint:disable-next-line:deprecation */
+ (mockInjectorService.get).and.returnValue(mockXSRFService);
+ loggingService = new LoggingServiceHelper(mockStore, appConfig, mockInjectorService);
// reset the calls counter because there is a log in the constructor
mockStore.dispatch.calls.reset();
});
@@ -66,7 +71,7 @@ describe("Service: StarkLoggingService", () => {
for (const invalidValue of invalidValues) {
appConfig.loggingFlushPersistSize = invalidValue;
- expect(() => new LoggingServiceHelper(mockStore, appConfig /*, mockXSRFService*/)).toThrowError(/loggingFlushPersistSize/);
+ expect(() => new LoggingServiceHelper(mockStore, appConfig, mockInjectorService)).toThrowError(/loggingFlushPersistSize/);
}
});
@@ -74,7 +79,7 @@ describe("Service: StarkLoggingService", () => {
appConfig.loggingFlushDisabled = false;
appConfig.backends.delete("logging");
- expect(() => new LoggingServiceHelper(mockStore, appConfig /*, mockXSRFService*/)).toThrowError(/backend/);
+ expect(() => new LoggingServiceHelper(mockStore, appConfig, mockInjectorService)).toThrowError(/backend/);
});
it("should generate a new correlation id", () => {
@@ -308,8 +313,8 @@ describe("Service: StarkLoggingService", () => {
});
class LoggingServiceHelper extends StarkLoggingServiceImpl {
- public constructor(store: Store, appConfig: StarkApplicationConfig /*, xsrfService: StarkXSRFService*/) {
- super(store, appConfig /*, xsrfService*/);
+ public constructor(store: Store, appConfig: StarkApplicationConfig, injector: Injector) {
+ super(store, appConfig, injector);
}
public constructLogMessageHelper(messageType: StarkLogMessageType, ...args: any[]): StarkLogMessage {
diff --git a/packages/stark-core/src/modules/logging/services/logging.service.ts b/packages/stark-core/src/modules/logging/services/logging.service.ts
index 2eed1f9589..efd33c86b8 100644
--- a/packages/stark-core/src/modules/logging/services/logging.service.ts
+++ b/packages/stark-core/src/modules/logging/services/logging.service.ts
@@ -1,19 +1,17 @@
/* tslint:disable:completed-docs*/
import uuid from "uuid";
-
import { Serialize } from "cerialize";
-
import { select, Store } from "@ngrx/store";
+import { Inject, Injectable, Injector } from "@angular/core";
import { Observable, Subject } from "rxjs";
-import { Inject, Injectable } from "@angular/core";
-
import { StarkLoggingService, starkLoggingServiceName } from "./logging.service.intf";
import { STARK_APP_CONFIG, StarkApplicationConfig } from "../../../configuration/entities/application";
import { StarkBackend } from "../../http/entities/backend";
import { StarkCoreApplicationState } from "../../../common/store";
import { StarkHttpStatusCodes } from "../../http/enumerators";
import { StarkHttpHeaders } from "../../http/constants";
+import { STARK_XSRF_SERVICE, StarkXSRFService } from "../../xsrf/services/xsrf.service.intf";
import { StarkLogging, StarkLoggingImpl, StarkLogMessage, StarkLogMessageImpl, StarkLogMessageType } from "../entities";
import { StarkFlushLogMessages, StarkLogMessageAction } from "../actions";
import { selectStarkLogging } from "../reducers";
@@ -24,6 +22,8 @@ import { StarkConfigurationUtil } from "../../../util/configuration.util";
*/
const _noop: Function = require("lodash/noop");
+const xsrfServiceNotFound: "not provided" = "not provided";
+
/**
* @ignore
* @ngdoc service
@@ -43,14 +43,14 @@ export class StarkLoggingServiceImpl implements StarkLoggingService {
private consoleError: Function;
private starkLogging: StarkLogging;
/** @internal */
+ private _xsrfService?: StarkXSRFService | typeof xsrfServiceNotFound;
+ /** @internal */
private _correlationId: string;
- // FIXME: uncomment these lines once XSRF Service is implemented
public constructor(
private store: Store,
- @Inject(STARK_APP_CONFIG)
- private appConfig: StarkApplicationConfig /*,
- @Inject(starkXSRFServiceName) private xsrfService: StarkXSRFService*/
+ @Inject(STARK_APP_CONFIG) private appConfig: StarkApplicationConfig,
+ private injector: Injector
) {
// ensuring that the app config is valid before doing anything
StarkConfigurationUtil.validateConfig(this.appConfig, ["logging", "http"], starkLoggingServiceName);
@@ -214,8 +214,9 @@ export class StarkLoggingServiceImpl implements StarkLoggingService {
// IE "Access is denied" error: https://stackoverflow.com/questions/22098259/access-denied-in-ie-10-and-11-when-ajax-target-is-localhost
try {
xhr.open("POST", url, async);
- // FIXME: uncomment when XSRF service is implemented
- // this.xsrfService.configureXHR(xhr);
+ if (this.xsrfService) {
+ this.xsrfService.configureXHR(xhr);
+ }
xhr.setRequestHeader(StarkHttpHeaders.CONTENT_TYPE, "application/json");
xhr.send(serializedData);
} catch (e) {
@@ -259,4 +260,23 @@ export class StarkLoggingServiceImpl implements StarkLoggingService {
return logFn.apply(console, consoleArgs);
};
}
+
+ /**
+ * Gets the StarkXSRFService from the Injector (this is tried only once).
+ * It returns 'undefined' if the service is not found (the XSRF module is not imported in the app).
+ */
+ private get xsrfService(): StarkXSRFService | undefined {
+ if (typeof this._xsrfService === "undefined") {
+ // The StarkXSRFService should be resolved at runtime to prevent the Angular DI circular dependency errors
+ try {
+ this._xsrfService = this.injector.get(STARK_XSRF_SERVICE);
+ return this._xsrfService;
+ } catch (exception) {
+ this._xsrfService = xsrfServiceNotFound;
+ return undefined;
+ }
+ }
+
+ return this._xsrfService !== xsrfServiceNotFound ? this._xsrfService : undefined;
+ }
}
diff --git a/packages/stark-core/src/modules/xsrf.ts b/packages/stark-core/src/modules/xsrf.ts
new file mode 100644
index 0000000000..7d74634060
--- /dev/null
+++ b/packages/stark-core/src/modules/xsrf.ts
@@ -0,0 +1,3 @@
+export * from "./xsrf/interceptors";
+export * from "./xsrf/services";
+export * from "./xsrf/xsrf.module";
diff --git a/packages/stark-core/src/modules/xsrf/interceptors.ts b/packages/stark-core/src/modules/xsrf/interceptors.ts
new file mode 100644
index 0000000000..9d0e959e16
--- /dev/null
+++ b/packages/stark-core/src/modules/xsrf/interceptors.ts
@@ -0,0 +1 @@
+export * from "./interceptors/http-xsrf.interceptor";
diff --git a/packages/stark-core/src/modules/xsrf/interceptors/http-xsrf.interceptor.ts b/packages/stark-core/src/modules/xsrf/interceptors/http-xsrf.interceptor.ts
new file mode 100644
index 0000000000..1aaf8f82ea
--- /dev/null
+++ b/packages/stark-core/src/modules/xsrf/interceptors/http-xsrf.interceptor.ts
@@ -0,0 +1,34 @@
+import { Inject, Injectable } from "@angular/core";
+import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
+import { Observable } from "rxjs";
+import { tap } from "rxjs/operators";
+import { StarkXSRFService, STARK_XSRF_SERVICE } from "../services/xsrf.service.intf";
+
+/**
+ * Angular Http interceptor that adds the XSRF configuration to every state-changing request (POST,PUT,PATCH and DELETE)
+ * and stores the XSRF token from every response.
+ *
+ * Defined in the HttpClientXsrfModule set in packages/stark-core/src/modules/http/http.module.ts
+ */
+@Injectable()
+export class StarkXSRFHttpInterceptor implements HttpInterceptor {
+ public constructor(@Inject(STARK_XSRF_SERVICE) public xsrfService: StarkXSRFService) {}
+
+ /**
+ * @param request - The intercepted outgoing `HttpRequest`
+ * @param next - The next request handler where the `HttpRequest` will be forwarded to
+ * @returns The modified `HttpRequest` with the XSRF configuration enabled.
+ */
+ public intercept(request: HttpRequest, next: HttpHandler): Observable> {
+ const xsrfProtectedRequest: HttpRequest = this.xsrfService.configureHttpRequest(request);
+
+ return next
+ .handle(xsrfProtectedRequest) // pass request through to the next request handler
+ .pipe(
+ // the Http response is intercepted in order to extract and store the XSRF token via the XSRF service
+ tap((_httpResponse: HttpEvent) => {
+ this.xsrfService.storeXSRFToken();
+ })
+ );
+ }
+}
diff --git a/packages/stark-core/src/modules/xsrf/services.ts b/packages/stark-core/src/modules/xsrf/services.ts
new file mode 100644
index 0000000000..a7736f4f05
--- /dev/null
+++ b/packages/stark-core/src/modules/xsrf/services.ts
@@ -0,0 +1,3 @@
+export * from "./services/xsrf.service";
+export * from "./services/xsrf-config.intf";
+export { STARK_XSRF_SERVICE, StarkXSRFService } from "./services/xsrf.service.intf";
diff --git a/packages/stark-core/src/modules/xsrf/services/xsrf-config.intf.ts b/packages/stark-core/src/modules/xsrf/services/xsrf-config.intf.ts
new file mode 100644
index 0000000000..c64d358ea9
--- /dev/null
+++ b/packages/stark-core/src/modules/xsrf/services/xsrf-config.intf.ts
@@ -0,0 +1,35 @@
+import { InjectionToken } from "@angular/core";
+import { Observable } from "rxjs";
+
+/**
+ * The InjectionToken version of the config name
+ */
+export const STARK_XSRF_CONFIG: InjectionToken = new InjectionToken("StarkXSRFConfig");
+
+/**
+ * Alternative literal object to define the waitBeforePinging function and its DI dependencies
+ */
+export interface StarkXSRFWaitBeforePingingLiteral {
+ /**
+ * Array of Dependency Injection tokens for the dependencies of the waitBeforePingingFn.
+ */
+ deps: any[];
+
+ /**
+ * Function that will be called by the XSRF service passing the necessary dependencies to get the corresponding Promise/Observable
+ * that the service should wait for before pinging all the backends.
+ */
+ waitBeforePingingFn: (...deps: any[]) => Promise | PromiseLike | Observable;
+}
+
+/**
+ * Definition of the configuration object for the Stark XSRF service
+ */
+export interface StarkXSRFConfig {
+ /**
+ * Function that will be called by the XSRF service to get the corresponding Promise/Observable
+ * that the service should wait for before pinging all the backends.
+ * Alternatively, this can be defined as a {@link StarkXSRFWaitBeforePingingLiteral|literal}
+ */
+ waitBeforePinging?: (() => Promise | PromiseLike | Observable) | StarkXSRFWaitBeforePingingLiteral;
+}
diff --git a/packages/stark-core/src/modules/xsrf/services/xsrf.service.intf.ts b/packages/stark-core/src/modules/xsrf/services/xsrf.service.intf.ts
new file mode 100644
index 0000000000..459e4d578f
--- /dev/null
+++ b/packages/stark-core/src/modules/xsrf/services/xsrf.service.intf.ts
@@ -0,0 +1,52 @@
+import { InjectionToken } from "@angular/core";
+import { HttpRequest } from "@angular/common/http";
+
+/**
+ * The name of the service in case an injection is needed
+ */
+export const starkXSRFServiceName: string = "StarkXSRFService";
+/**
+ * The InjectionToken version of the service name
+ */
+export const STARK_XSRF_SERVICE: InjectionToken = new InjectionToken(starkXSRFServiceName);
+
+/**
+ * Stark XSRF Service.
+ * Service to get/store the XSRF token to be used with the different backends.
+ */
+export interface StarkXSRFService {
+ /**
+ * Add the necessary options to the XHR config in order to enable XSRF protection.
+ * Since the service will add the XSRF header to the XHR object, this method must be called after calling the XHR open() method because
+ * headers cannot be set before open(). See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader
+ * This method should be used for those HTTP state-changing requests (POST, PUT, PATCH or DELETE) which are not performed
+ * using StarkHttpService or Angular raw $http
+ * @param xhr - The XHR object to be configured
+ */
+ configureXHR(xhr: XMLHttpRequest): void;
+
+ /**
+ * Return a new `HttpRequest` including the necessary options for state-changing requests (POST, PUT, PATCH or DELETE)
+ * in order to enable XSRF protection.
+ * Logs a warning whenever there is no XSRF token to be sent in such requests
+ * @param request - The Angular `HttpRequest` to be modified
+ * @returns The modified Angular `HttpRequest`
+ */
+ configureHttpRequest(request: HttpRequest): HttpRequest;
+
+ /**
+ * Get the current XSRF token (in case there is one already stored)
+ */
+ getXSRFToken(): string | undefined;
+
+ /**
+ * Store the token from the current XSRF cookie
+ */
+ storeXSRFToken(): void;
+
+ /**
+ * Trigger a GET Http request to all the backends in order to get their XSRF tokens.
+ * Then the response is intercepted by the XSRF Http Interceptor to store the token from the current XSRF cookie
+ */
+ pingBackends(): void;
+}
diff --git a/packages/stark-core/src/modules/xsrf/services/xsrf.service.spec.ts b/packages/stark-core/src/modules/xsrf/services/xsrf.service.spec.ts
new file mode 100644
index 0000000000..ff71134397
--- /dev/null
+++ b/packages/stark-core/src/modules/xsrf/services/xsrf.service.spec.ts
@@ -0,0 +1,374 @@
+/*tslint:disable:completed-docs*/
+import { HttpClient, HttpErrorResponse, HttpRequest, HttpResponse } from "@angular/common/http";
+import { fakeAsync, tick } from "@angular/core/testing";
+import { Injector } from "@angular/core";
+import { Observable, of, Subject, throwError } from "rxjs";
+import { StarkHttpHeaders } from "../../http/constants";
+import { StarkLoggingService } from "../../logging/services";
+import { StarkXSRFServiceImpl } from "./xsrf.service";
+import { StarkXSRFConfig } from "./xsrf-config.intf";
+import { StarkApplicationConfig, StarkApplicationConfigImpl } from "../../../configuration/entities";
+import { StarkBackend, StarkBackendAuthenticationTypes } from "../../http/entities";
+import { MockStarkLoggingService } from "../../logging/testing/logging.mock";
+import Spy = jasmine.Spy;
+import SpyObj = jasmine.SpyObj;
+import createSpyObj = jasmine.createSpyObj;
+import CallInfo = jasmine.CallInfo;
+
+/* tslint:disable-next-line:no-big-function */
+describe("Service: StarkXSRFService", () => {
+ let xsrfService: StarkXSRFServiceHelper;
+ let appConfig: StarkApplicationConfig;
+ let mockDocument: Pick;
+ let mockInjectorService: SpyObj;
+ let mockXsrfConfig: StarkXSRFConfig;
+
+ const mockLogger: StarkLoggingService = new MockStarkLoggingService();
+ const httpMock: SpyObj = createSpyObj("HttpClient", ["get"]);
+ const mockXSRFToken: string = "dummy xsrf token";
+ const dummyHeader: string = "X-DUMMY-HEADER";
+
+ const mockBackend1: StarkBackend = {
+ name: "dummy backend 1",
+ url: "dummy/url",
+ authenticationType: StarkBackendAuthenticationTypes.PUBLIC,
+ devAuthenticationEnabled: false,
+ devAuthenticationRolePrefix: ""
+ };
+ const mockBackend2: StarkBackend = { ...mockBackend1, name: "dummy backend 2", url: "other/url" };
+ const mockBackend3: StarkBackend = { ...mockBackend1, name: "dummy backend 3", url: "another/url" };
+
+ // FIXME: this tslint disable flag is due to a bug in 'no-element-overwrite' rule. Remove it once it is solved
+ /* tslint:disable:no-element-overwrite */
+ beforeEach(() => {
+ appConfig = new StarkApplicationConfigImpl();
+ appConfig.backends = new Map();
+ appConfig.backends.set(mockBackend1.name, mockBackend1);
+ appConfig.backends.set(mockBackend2.name, mockBackend2);
+ appConfig.backends.set(mockBackend3.name, mockBackend3);
+ mockDocument = { cookie: "" };
+ mockInjectorService = jasmine.createSpyObj("injector,", ["get"]);
+ mockXsrfConfig = {};
+
+ (mockLogger.error).calls.reset();
+ (mockLogger.warn).calls.reset();
+ httpMock.get.calls.reset();
+
+ xsrfService = new StarkXSRFServiceHelper(appConfig, mockLogger, httpMock, mockDocument, mockInjectorService, mockXsrfConfig);
+ });
+ /* tslint:enable:no-element-overwrite */
+
+ describe("configureXHR", () => {
+ it("should add the necessary options to the XHR object in order to enable XSRF protection", () => {
+ spyOn(xsrfService, "getXSRFToken").and.returnValue(mockXSRFToken);
+
+ const mockXHR: XMLHttpRequest = new XMLHttpRequest();
+ mockXHR.open("GET", "some/url");
+
+ spyOn(mockXHR, "setRequestHeader");
+
+ xsrfService.configureXHR(mockXHR);
+
+ expect(xsrfService.getXSRFToken).toHaveBeenCalledTimes(1);
+ expect(mockXHR.setRequestHeader).toHaveBeenCalledTimes(1);
+ expect(mockXHR.setRequestHeader).toHaveBeenCalledWith(StarkHttpHeaders.XSRF_TOKEN, mockXSRFToken);
+ expect(mockXHR.withCredentials).toBe(true);
+ });
+
+ it("should NOT add any options to the XHR object if the XSRF token is not yet stored", () => {
+ spyOn(xsrfService, "getXSRFToken").and.returnValue(undefined);
+
+ const mockXHR: XMLHttpRequest = new XMLHttpRequest();
+ mockXHR.open("GET", "some/url");
+
+ spyOn(mockXHR, "setRequestHeader");
+
+ xsrfService.configureXHR(mockXHR);
+
+ expect(xsrfService.getXSRFToken).toHaveBeenCalledTimes(1);
+ expect(mockXHR.setRequestHeader).not.toHaveBeenCalled();
+ expect(mockXHR.withCredentials).toBe(false);
+ });
+
+ it("should THROW an error when it was called without calling the XHE open() method before", () => {
+ const mockXHR: XMLHttpRequest = new XMLHttpRequest();
+
+ expect(() => xsrfService.configureXHR(mockXHR)).toThrowError(/open\(\) method has not been invoked/);
+ });
+ });
+
+ describe("configureHttpRequest", () => {
+ it("should create a new Angular HttpRequest with the XSRF protection enabled if the HTTP method is POST, PUT, PATCH or DELETE", () => {
+ spyOn(xsrfService, "getXSRFToken").and.returnValue(mockXSRFToken);
+
+ const stateChangingMethods: string[] = ["POST", "PUT", "PATCH", "DELETE"];
+
+ function headersShouldBeInitialized(httpMethod: string): boolean {
+ return httpMethod === "PUT" || httpMethod === "PATCH";
+ }
+
+ for (const stateChangingMethod of stateChangingMethods) {
+ (xsrfService.getXSRFToken).calls.reset();
+
+ let mockHttpRequest: HttpRequest = new HttpRequest(stateChangingMethod, "dummy/url");
+
+ if (headersShouldBeInitialized(stateChangingMethod)) {
+ mockHttpRequest = mockHttpRequest.clone({ headers: mockHttpRequest.headers.set(dummyHeader, "dummy value") });
+ }
+
+ const protectedConfig: HttpRequest = xsrfService.configureHttpRequest(mockHttpRequest);
+
+ expect(xsrfService.getXSRFToken).toHaveBeenCalledTimes(1);
+ expect(protectedConfig).not.toBe(mockHttpRequest);
+ expect(protectedConfig).not.toEqual(mockHttpRequest);
+ expect(protectedConfig.withCredentials).toBe(true);
+ expect(protectedConfig.headers).toBeDefined();
+ expect(protectedConfig.headers.get(StarkHttpHeaders.XSRF_TOKEN)).toBe(mockXSRFToken);
+
+ if (headersShouldBeInitialized(stateChangingMethod)) {
+ expect(protectedConfig.headers.get(dummyHeader)).toBe("dummy value");
+ }
+ }
+ });
+
+ it("should leave the HttpRequest 'as is' if the XSRF token is not yet stored", () => {
+ spyOn(xsrfService, "getXSRFToken").and.returnValue(undefined);
+
+ const stateChangingMethods: string[] = ["POST", "PUT", "PATCH", "DELETE"];
+
+ for (const stateChangingMethod of stateChangingMethods) {
+ (xsrfService.getXSRFToken).calls.reset();
+
+ const mockHttpRequest: HttpRequest = new HttpRequest(stateChangingMethod, "dummy/url");
+
+ const protectedRequest: HttpRequest = xsrfService.configureHttpRequest(mockHttpRequest);
+
+ expect(xsrfService.getXSRFToken).toHaveBeenCalledTimes(1);
+ expect(protectedRequest).toEqual(mockHttpRequest.clone({ withCredentials: true }));
+ expect(protectedRequest.withCredentials).toBe(true);
+ expect(protectedRequest.headers.keys().length).toBe(0);
+ }
+ });
+
+ it("should only add 'withCredentials: true' if the HTTP method is not POST, PUT, PATCH nor DELETE", () => {
+ spyOn(xsrfService, "getXSRFToken");
+
+ const nonStateChangingMethods: string[] = ["GET", "HEAD", "CONNECT", "OPTIONS", "TRACE"];
+
+ function headersShouldBeInitialized(httpMethod: string): boolean {
+ return httpMethod === "GET" || httpMethod === "OPTIONS";
+ }
+
+ for (const nonStateChangingMethod of nonStateChangingMethods) {
+ let mockHttpRequest: HttpRequest = new HttpRequest(nonStateChangingMethod, "dummy/url");
+
+ if (headersShouldBeInitialized(nonStateChangingMethod)) {
+ mockHttpRequest = mockHttpRequest.clone({ headers: mockHttpRequest.headers.set(dummyHeader, "some value") });
+ }
+
+ const protectedRequest: HttpRequest = xsrfService.configureHttpRequest(mockHttpRequest);
+
+ expect(xsrfService.getXSRFToken).not.toHaveBeenCalled();
+ expect(protectedRequest).toEqual(mockHttpRequest.clone({ withCredentials: true }));
+ expect(protectedRequest.withCredentials).toBe(true);
+
+ if (headersShouldBeInitialized(nonStateChangingMethod)) {
+ expect(protectedRequest.headers.keys().length).toBeGreaterThan(0);
+ expect(protectedRequest.headers.get(dummyHeader)).toBe("some value");
+ expect(protectedRequest.headers.get(StarkHttpHeaders.XSRF_TOKEN)).toBeNull();
+ } else {
+ expect(protectedRequest.headers.keys().length).toBe(0);
+ }
+ }
+ });
+ });
+
+ describe("getXSRFToken", () => {
+ it("should return the XSRF token in case there is one already stored", () => {
+ xsrfService.setCurrentToken(mockXSRFToken);
+
+ const xsrfToken: string = xsrfService.getXSRFToken();
+
+ expect(xsrfToken).toBe(mockXSRFToken);
+ expect(mockLogger.warn).not.toHaveBeenCalled();
+ });
+
+ it("should overwrite the XSRF cookie with the XSRF token that is already stored", () => {
+ xsrfService.setCurrentToken(mockXSRFToken);
+ spyOn(xsrfService, "setXSRFCookie").and.callThrough();
+
+ const xsrfToken: string = xsrfService.getXSRFToken();
+
+ expect(xsrfToken).toBe(mockXSRFToken);
+ expect(xsrfService.setXSRFCookie).toHaveBeenCalledTimes(1);
+ expect(xsrfService.setXSRFCookie).toHaveBeenCalledWith(xsrfToken);
+ expect(mockDocument.cookie.length).toBeGreaterThan(0);
+ const cookieOptions: any[] = mockDocument.cookie.split(";");
+ expect(cookieOptions.length).toBe(3);
+ expect(cookieOptions[0]).toBe(xsrfService.getXsrfCookieName() + "=" + mockXSRFToken);
+ expect(cookieOptions[1]).toBe("path='/'");
+ expect(cookieOptions[2]).toMatch(new RegExp("expires=.*(" + new Date().getFullYear() + ")"));
+ });
+
+ it("should return undefined and log a warning in case there is no XSRF token yet", () => {
+ xsrfService.setCurrentToken(undefined);
+
+ const xsrfToken: undefined = xsrfService.getXSRFToken();
+
+ expect(xsrfToken).toBeUndefined();
+ expect(mockLogger.warn).toHaveBeenCalledTimes(1);
+ const warningMessage: string = (mockLogger.warn).calls.argsFor(0)[0];
+ expect(warningMessage).toContain("no XSRF token found");
+ });
+ });
+
+ describe("storeXSRFToken", () => {
+ it("should store the XSRF token coming in the XSRF cookie if it has not been stored yet", () => {
+ xsrfService.setCurrentToken(undefined);
+ spyOn(xsrfService, "getXSRFCookie").and.returnValue(mockXSRFToken);
+
+ xsrfService.storeXSRFToken();
+
+ expect(xsrfService.getXSRFCookie).toHaveBeenCalledTimes(1);
+ expect(xsrfService.getCurrentToken()).toBe(mockXSRFToken);
+ });
+
+ it("should store an undefined value if it has not been stored yet and the XSRF cookie does not exist or it is empty", () => {
+ xsrfService.setCurrentToken(undefined);
+ spyOn(xsrfService, "getXSRFCookie").and.returnValues(undefined, "");
+
+ xsrfService.storeXSRFToken();
+
+ expect(xsrfService.getXSRFCookie).toHaveBeenCalledTimes(1);
+ expect(xsrfService.getCurrentToken()).toBeUndefined();
+
+ (xsrfService.getXSRFCookie).calls.reset();
+ xsrfService.storeXSRFToken();
+
+ expect(xsrfService.getXSRFCookie).toHaveBeenCalledTimes(1);
+ expect(xsrfService.getCurrentToken()).toBeUndefined();
+ });
+
+ it("should just overwrite the XSRF cookie with the XSRF token that is already stored", () => {
+ xsrfService.setCurrentToken(mockXSRFToken);
+ spyOn(xsrfService, "setXSRFCookie").and.callThrough();
+
+ xsrfService.storeXSRFToken();
+
+ expect(xsrfService.setXSRFCookie).toHaveBeenCalledTimes(1);
+ expect(xsrfService.setXSRFCookie).toHaveBeenCalledWith(xsrfService.getCurrentToken());
+ expect(mockDocument.cookie.length).toBeGreaterThan(0);
+ const cookieOptions: any[] = mockDocument.cookie.split(";");
+ expect(cookieOptions.length).toBe(3);
+ expect(cookieOptions[0]).toBe(xsrfService.getXsrfCookieName() + "=" + mockXSRFToken);
+ expect(cookieOptions[1]).toBe("path='/'");
+ expect(cookieOptions[2]).toMatch(new RegExp("expires=.*(" + new Date().getFullYear() + ")"));
+ });
+ });
+
+ describe("pingBackends", () => {
+ it("should trigger an HTTP call to every backend defined in the application configuration", fakeAsync(() => {
+ httpMock.get.and.returnValue(of(new HttpResponse({ body: "ping OK" })));
+
+ xsrfService.pingBackends();
+ tick();
+
+ expect(httpMock.get).toHaveBeenCalledTimes(appConfig.backends.size);
+ const httpCalls: CallInfo[] = httpMock.get.calls.all();
+ let callIndex: number = 0;
+
+ appConfig.backends.forEach((backendConfig: StarkBackend) => {
+ expect(httpCalls[callIndex].args[0]).toBe(backendConfig.url);
+ expect(httpCalls[callIndex].args[1]).toEqual({ observe: "response", responseType: "text" });
+ callIndex++;
+ });
+
+ expect(mockLogger.error).not.toHaveBeenCalled();
+ }));
+
+ it("should log an error when the HTTP call to a backend failed", fakeAsync(() => {
+ const failingBackends: StarkBackend[] = [mockBackend1, mockBackend3];
+
+ httpMock.get.and.callFake((url: string) => {
+ if (failingBackends.map((failingBackend: StarkBackend) => failingBackend.url).indexOf(url) !== -1) {
+ return throwError(new HttpErrorResponse({ error: "ping failed" }));
+ } else {
+ return of(new HttpResponse({ body: "ping OK" }));
+ }
+ });
+
+ xsrfService.pingBackends();
+ tick();
+
+ expect(httpMock.get).toHaveBeenCalledTimes(appConfig.backends.size);
+ const httpCalls: CallInfo[] = httpMock.get.calls.all();
+ let httpCallIdx: number = 0;
+
+ appConfig.backends.forEach((backendConfig: StarkBackend) => {
+ expect(httpCalls[httpCallIdx].args[0]).toBe(backendConfig.url);
+ expect(httpCalls[httpCallIdx].args[1]).toEqual({ observe: "response", responseType: "text" });
+ httpCallIdx++;
+ });
+
+ expect(mockLogger.error).toHaveBeenCalledTimes(failingBackends.length);
+ const logErrorCalls: CallInfo[] = (mockLogger.error).calls.all();
+ let logErrorCallIdx: number = 0;
+
+ for (const failingBackend of failingBackends) {
+ expect(logErrorCalls[logErrorCallIdx].args[0]).toContain(failingBackend.name);
+ logErrorCallIdx++;
+ }
+ }));
+
+ it("should NOT trigger any HTTP call until the waitBeforePinging observable emits", () => {
+ const mockWaitBeforePinging$: Subject = new Subject();
+ spyOn(xsrfService, "getWaitBeforePingingObs").and.returnValue(mockWaitBeforePinging$);
+
+ xsrfService.pingBackends();
+
+ expect(httpMock.get).not.toHaveBeenCalled();
+
+ mockWaitBeforePinging$.next("stop waiting");
+ mockWaitBeforePinging$.complete();
+
+ expect(httpMock.get).toHaveBeenCalledTimes(appConfig.backends.size);
+ });
+ });
+
+ class StarkXSRFServiceHelper extends StarkXSRFServiceImpl {
+ public constructor(
+ applicationConfig: StarkApplicationConfig,
+ logger: StarkLoggingService,
+ httpClient: HttpClient,
+ document: Document,
+ injector: Injector,
+ config: StarkXSRFConfig
+ ) {
+ super(applicationConfig, logger, httpClient, document, injector, config);
+ }
+
+ public getXSRFCookie(): string | undefined {
+ return super.getXSRFCookie();
+ }
+
+ public setXSRFCookie(xsrfToken: string): void {
+ super.setXSRFCookie(xsrfToken);
+ }
+
+ public getWaitBeforePingingObs(): Observable {
+ return super.getWaitBeforePingingObs();
+ }
+
+ public getXsrfCookieName(): string {
+ return this.xsrfCookieName;
+ }
+
+ public getCurrentToken(): string | undefined {
+ return this.currentToken;
+ }
+
+ public setCurrentToken(token: string | undefined): void {
+ this.currentToken = token;
+ }
+ }
+});
diff --git a/packages/stark-core/src/modules/xsrf/services/xsrf.service.ts b/packages/stark-core/src/modules/xsrf/services/xsrf.service.ts
new file mode 100644
index 0000000000..229a41e387
--- /dev/null
+++ b/packages/stark-core/src/modules/xsrf/services/xsrf.service.ts
@@ -0,0 +1,210 @@
+/* tslint:disable:completed-docs*/
+import { Inject, Injectable, Injector } from "@angular/core";
+import { DOCUMENT } from "@angular/common";
+import { HttpClient, HttpErrorResponse, HttpHeaders, HttpRequest } from "@angular/common/http";
+import moment from "moment";
+import { Observable, of, from } from "rxjs";
+import { take } from "rxjs/operators";
+import { StarkXSRFService, starkXSRFServiceName } from "./xsrf.service.intf";
+import { STARK_XSRF_CONFIG, StarkXSRFConfig } from "./xsrf-config.intf";
+import { StarkHttpHeaders } from "../../http/constants";
+import { StarkHttpStatusCodes } from "../../http/enumerators";
+import { STARK_APP_CONFIG, StarkApplicationConfig } from "../../../configuration/entities";
+import { StarkBackend, StarkHttpErrorWrapper, StarkHttpErrorWrapperImpl } from "../../http/entities";
+import { StarkLoggingService, STARK_LOGGING_SERVICE } from "../../logging/services/logging.service.intf";
+
+/**
+ * Service to get/store the XSRF token to be used with the different backends.
+ * It also adds the XSRF configuration to XHR objects for those HTTP requests not performed using StarkHttpService or Angular's HttpClient.
+ */
+@Injectable()
+export class StarkXSRFServiceImpl implements StarkXSRFService {
+ protected xsrfCookieName: string = "XSRF-TOKEN";
+ protected currentToken: string | undefined;
+
+ public constructor(
+ @Inject(STARK_APP_CONFIG) public appConfig: StarkApplicationConfig,
+ @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService,
+ private httpClient: HttpClient,
+ @Inject(DOCUMENT) public document: Document,
+ private injector: Injector,
+ @Inject(STARK_XSRF_CONFIG) public configOptions?: StarkXSRFConfig
+ ) {
+ this.logger.debug(starkXSRFServiceName + " loaded");
+ }
+
+ public configureXHR(xhr: XMLHttpRequest): void {
+ // in order to be able to configure the XHR object, we should call the setRequestHeader to add the XSRF header
+ // however the open() method should be called first, so we throw an error if that is not case
+ // see: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader
+ if (xhr.readyState === XMLHttpRequest.OPENED) {
+ const xsrfToken: string | undefined = this.getXSRFToken();
+
+ if (typeof xsrfToken !== "undefined") {
+ // Enforce the 'withCredentials' property flag on every XHR object.
+ // We leverage "credentialed" requests that are aware of HTTP cookies (necessary for XSRF to work with multiple backends)
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Requests_with_credentials
+ xhr.withCredentials = true;
+
+ xhr.setRequestHeader(StarkHttpHeaders.XSRF_TOKEN, xsrfToken);
+ }
+ } else {
+ throw new Error(
+ starkXSRFServiceName +
+ ": cannot set headers to XHR object because its open() method has not been invoked.\n" +
+ "Make sure that the XHR open() method is called first before calling " +
+ starkXSRFServiceName +
+ " configureXHR()"
+ );
+ }
+ }
+
+ public configureHttpRequest(request: HttpRequest): HttpRequest {
+ if (request.method.match(/POST|PUT|PATCH|DELETE/)) {
+ const xsrfToken: string | undefined = this.getXSRFToken();
+
+ if (typeof xsrfToken !== "undefined") {
+ const newHeaders: HttpHeaders = request.headers.set(StarkHttpHeaders.XSRF_TOKEN, xsrfToken);
+
+ return request.clone({
+ headers: newHeaders,
+ // Enforce the 'withCredentials' property flag on every XHR object created by Angular $http.
+ // We leverage "credentialed" requests that are aware of HTTP cookies (necessary for XSRF to work with multiple backends)
+ // https://angular.io/api/common/http/HttpRequest#withCredentials
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Requests_with_credentials
+ withCredentials: true
+ });
+ }
+ }
+
+ // in any case the "withCredentials: true" should be added to ALL requests, otherwise the browser won't accept the XSRF cookie from the backend!
+ // see: https://angular.io/api/common/http/HttpRequest#withCredentials
+ // see: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Requests_with_credentials
+ return request.clone({ withCredentials: true });
+ }
+
+ public getXSRFToken(): string | undefined {
+ const xsrfToken: string | undefined = this.currentToken;
+
+ if (typeof xsrfToken === "undefined") {
+ const errorMsg: string =
+ starkXSRFServiceName +
+ ": no XSRF token found. This could be due to:\n" +
+ "- the backend has not sent the XSRF token properly, either the cookie was not sent or it has a different name\n" +
+ "- the application did not store the XSRF token correctly, either it has a different name or it comes from a different origin";
+
+ this.logger.warn(errorMsg);
+ // could throw an error: throw new Error(errorMsg);
+ } else {
+ // overwrite the cookie with the current token to ensure that we always send the same token
+ // regardless of the new tokens sent by the backend(s) in every response
+ this.setXSRFCookie(xsrfToken);
+ }
+
+ return xsrfToken;
+ }
+
+ public storeXSRFToken(): void {
+ if (this.currentToken) {
+ // overwrite the cookie with the token we stored (we don't care about the rest of tokens but just the one we stored)
+ this.setXSRFCookie(this.currentToken);
+ } else {
+ // store the token only if it is not stored yet
+ const xsrfCookie: string | undefined = this.getXSRFCookie();
+ this.currentToken = xsrfCookie && xsrfCookie !== "" ? xsrfCookie : undefined;
+ }
+ }
+
+ public pingBackends(): void {
+ const backendsMap: Map = this.appConfig.getBackends();
+
+ const waitFor$: Observable = this.getWaitBeforePingingObs();
+
+ waitFor$.pipe(take(1)).subscribe(() => {
+ backendsMap.forEach((backendConfig: StarkBackend) => {
+ // here the Angular HttpClient is used instead of the StarkHttpService because the response can be anything
+ // and the StarkHttpService expects only JSON responses causing it to throw an exception
+ this.httpClient
+ .get(backendConfig.url, {
+ observe: "response", // full response, not only the body
+ responseType: "text" // body as text to allow any kind of response and avoid having weird exceptions as with the StarkHttpService
+ })
+ .subscribe({
+ // error: (errorWrapper: StarkHttpErrorWrapper) => {
+ error: (errorResponse: HttpErrorResponse) => {
+ // the backend might return 404 Not Found, but it will still send the cookie
+ if (errorResponse.status !== StarkHttpStatusCodes.HTTP_404_NOT_FOUND) {
+ const httpResponseHeaders: Map = new Map();
+ for (const headerName of errorResponse.headers.keys()) {
+ httpResponseHeaders.set(headerName, errorResponse.headers.get(headerName));
+ }
+ const errorWrapper: StarkHttpErrorWrapper = new StarkHttpErrorWrapperImpl(
+ errorResponse,
+ httpResponseHeaders,
+ errorResponse.error
+ );
+
+ const errorMsg: string =
+ starkXSRFServiceName + ": ping sent to backend '" + backendConfig.name + "' failed.";
+ this.logger.error(errorMsg, errorWrapper);
+ }
+ }
+ });
+ });
+ });
+ }
+
+ /**
+ * Extracts the Promise/Observable that the service should wait for before pinging all the backends.
+ * Such Promise/Observable is extracted from the configuration object (if any) passed to the StarkXSRFModule.forRoot()
+ */
+ protected getWaitBeforePingingObs(): Observable {
+ if (this.configOptions && this.configOptions.waitBeforePinging) {
+ let waitBeforePingingFn: Function;
+ let waitBeforePingingDeps: any[] = [];
+
+ if (typeof this.configOptions.waitBeforePinging === "object") {
+ waitBeforePingingFn = this.configOptions.waitBeforePinging.waitBeforePingingFn;
+ // for a StarkXSRFWaitBeforePingingLiteral we should get all the DI dependencies via the Angular Injector
+ waitBeforePingingDeps = this.configOptions.waitBeforePinging.deps.map((diDependency: any) => {
+ return this.injector.get(diDependency);
+ });
+ } else {
+ waitBeforePingingFn = this.configOptions.waitBeforePinging;
+ }
+
+ return from(waitBeforePingingFn(...waitBeforePingingDeps) || ["no wait"]);
+ }
+
+ return of("no wait");
+ }
+
+ protected setXSRFCookie(xsrfToken: string): void {
+ const cookieExpiration: string = moment()
+ .add(40, "m")
+ .toDate()
+ .toUTCString(); // 40 minutes from now
+
+ const cookieAttributes: string[] = [`${this.xsrfCookieName}=${xsrfToken}`, `path='/'`, `expires=${cookieExpiration}`];
+
+ this.document.cookie = cookieAttributes.join(";");
+ }
+
+ // code taken from ngx-cookie-service library (https://github.com/7leads/ngx-cookie-service/blob/master/lib/cookie-service/cookie.service.ts)
+ protected getXSRFCookie(): string | undefined {
+ const cookieRegExp: RegExp = this.getCookieRegExp(encodeURIComponent(this.xsrfCookieName));
+ const result: RegExpExecArray | null = cookieRegExp.exec(this.document.cookie);
+
+ if (result) {
+ return decodeURIComponent(result[1]);
+ } else {
+ return undefined;
+ }
+ }
+
+ private getCookieRegExp(cookieName: string): RegExp {
+ const escapedName: string = cookieName.replace(/([\[\]\{\}\(\)\|\=\;\+\?\,\.\*\^\$])/gi, "\\$1");
+
+ return new RegExp("(?:^" + escapedName + "|;\\s*" + escapedName + ")=(.*?)(?:;|$)", "g");
+ }
+}
diff --git a/packages/stark-core/src/modules/xsrf/testing.ts b/packages/stark-core/src/modules/xsrf/testing.ts
new file mode 100644
index 0000000000..1cee51e911
--- /dev/null
+++ b/packages/stark-core/src/modules/xsrf/testing.ts
@@ -0,0 +1 @@
+export * from "./testing/xsrf.mock";
diff --git a/packages/stark-core/src/modules/xsrf/testing/xsrf.mock.ts b/packages/stark-core/src/modules/xsrf/testing/xsrf.mock.ts
new file mode 100644
index 0000000000..29a461e3de
--- /dev/null
+++ b/packages/stark-core/src/modules/xsrf/testing/xsrf.mock.ts
@@ -0,0 +1,18 @@
+import { HttpRequest } from "@angular/common/http";
+import { StarkXSRFService } from "@nationalbankbelgium/stark-core";
+
+/**
+ * Mock class of the StarkXSRFService interface.
+ * @link StarkXSRFService
+ */
+export class MockStarkXsrfService implements StarkXSRFService {
+ public configureHttpRequest: (request: HttpRequest) => HttpRequest = jasmine.createSpy("configureHttpRequest");
+
+ public configureXHR: (xhr: XMLHttpRequest) => void = jasmine.createSpy("configureXHR");
+
+ public getXSRFToken: () => string | undefined = jasmine.createSpy("getXSRFToken");
+
+ public pingBackends: () => void = jasmine.createSpy("pingBackends");
+
+ public storeXSRFToken: () => void = jasmine.createSpy("storeXSRFToken");
+}
diff --git a/packages/stark-core/src/modules/xsrf/xsrf.module.ts b/packages/stark-core/src/modules/xsrf/xsrf.module.ts
new file mode 100644
index 0000000000..5e5892ab43
--- /dev/null
+++ b/packages/stark-core/src/modules/xsrf/xsrf.module.ts
@@ -0,0 +1,62 @@
+import { ApplicationInitStatus, Inject, ModuleWithProviders, NgModule, Optional, SkipSelf } from "@angular/core";
+import { HTTP_INTERCEPTORS, HttpClientModule, HttpClientXsrfModule } from "@angular/common/http";
+import { from } from "rxjs";
+import { StarkXSRFServiceImpl, STARK_XSRF_SERVICE, StarkXSRFService, StarkXSRFConfig, STARK_XSRF_CONFIG } from "./services";
+import { StarkHttpHeaders } from "../http/constants";
+import { StarkXSRFHttpInterceptor } from "./interceptors/http-xsrf.interceptor";
+
+@NgModule({
+ imports: [
+ HttpClientModule,
+ HttpClientXsrfModule.withOptions({
+ // Name of cookie containing the XSRF token. Default value in Angular is 'XSRF-TOKEN'
+ // https://angular.io/guide/http#security-xsrf-protection
+ cookieName: "XSRF-TOKEN",
+ // Name of HTTP header to populate with the XSRF token. Default value in Angular is 'X-XSRF-TOKEN'.
+ // https://angular.io/guide/http#security-xsrf-protection
+ headerName: StarkHttpHeaders.XSRF_TOKEN
+ })
+ ]
+})
+export class StarkXSRFModule {
+ /**
+ * Instantiates the services only once since they should be singletons
+ * so the forRoot() should be called only by the AppModule
+ * @link https://angular.io/guide/singleton-services#forroot
+ * @param xsrfConfig - Object containing the configuration (if any) for the XSRF service
+ * @returns a module with providers
+ */
+ public static forRoot(xsrfConfig?: StarkXSRFConfig): ModuleWithProviders {
+ return {
+ ngModule: StarkXSRFModule,
+ providers: [
+ { provide: STARK_XSRF_CONFIG, useValue: xsrfConfig },
+ { provide: STARK_XSRF_SERVICE, useClass: StarkXSRFServiceImpl },
+ { provide: HTTP_INTERCEPTORS, useClass: StarkXSRFHttpInterceptor, multi: true } // Add the StarkXSRFHttpInterceptor as an Http interceptor to handle missing XSRF token
+ ]
+ };
+ }
+
+ /**
+ * Prevents this module from being re-imported
+ * @link https://angular.io/guide/singleton-services#prevent-reimport-of-the-coremodule
+ * @param parentModule - the parent module
+ */
+ public constructor(
+ @Optional()
+ @SkipSelf()
+ parentModule: StarkXSRFModule,
+ @Inject(STARK_XSRF_SERVICE) xsrfService: StarkXSRFService,
+ appInitStatus: ApplicationInitStatus
+ ) {
+ if (parentModule) {
+ throw new Error("StarkXSRFModule is already loaded. Import it in the AppModule only");
+ }
+
+ // this logic cannot be executed in an APP_INITIALIZER factory because the StarkXsrfService uses the StarkLoggingService
+ // which needs the "logging" state to be already defined in the Store (which NGRX defines internally via APP_INITIALIZER factories :p)
+ from(appInitStatus.donePromise).subscribe(() => {
+ xsrfService.pingBackends();
+ });
+ }
+}
diff --git a/packages/stark-core/testing/public_api.ts b/packages/stark-core/testing/public_api.ts
index 9c98543a29..eeb0bdc78b 100644
--- a/packages/stark-core/testing/public_api.ts
+++ b/packages/stark-core/testing/public_api.ts
@@ -8,5 +8,6 @@ export * from "../src/modules/logging/testing";
export * from "../src/modules/routing/testing";
export * from "../src/modules/session/testing";
export * from "../src/modules/user/testing";
+export * from "../src/modules/xsrf/testing";
// This file only reexports content of the `src/modules/**/testing` folders. Keep it that way.
diff --git a/showcase/src/app/app.module.ts b/showcase/src/app/app.module.ts
index 58d7a3b0bf..4eb62eff0f 100644
--- a/showcase/src/app/app.module.ts
+++ b/showcase/src/app/app.module.ts
@@ -18,9 +18,10 @@ import { MatCheckboxModule } from "@angular/material/checkbox";
import { MatListModule } from "@angular/material/list";
import { MatSidenavModule } from "@angular/material/sidenav";
import { MatTooltipModule } from "@angular/material/tooltip";
-import { SharedModule } from "./shared/shared.module";
import { DateAdapter } from "@angular/material/core";
-import { filter } from "rxjs/operators";
+import { SharedModule } from "./shared/shared.module";
+import { Observable, of } from "rxjs";
+import { filter, map } from "rxjs/operators";
import {
STARK_APP_CONFIG,
@@ -42,7 +43,8 @@ import {
StarkSettingsModule,
StarkSettingsService,
StarkUser,
- StarkUserModule
+ StarkUserModule,
+ StarkXSRFModule
} from "@nationalbankbelgium/stark-core";
import {
@@ -127,6 +129,22 @@ export function initRouterLog(router: UIRouter): Function {
return () => logRegisteredStates(router.stateService.get());
}
+export function getXsrfWaitBeforePinging(sessionService: StarkSessionService): Observable {
+ let waitFor$: Observable = of("production"); // no need to wait on production
+
+ if (ENV !== "production") {
+ // wait for the user to be logged in (useful when targeting a live backend on DEV)
+ waitFor$ = sessionService.getCurrentUser().pipe(
+ filter((user?: StarkUser) => typeof user !== "undefined"),
+ map(() => {
+ return "dev login";
+ })
+ );
+ }
+
+ return waitFor$;
+}
+
// Application Redux State
export interface State {
// reducer interfaces
@@ -193,6 +211,12 @@ export const metaReducers: MetaReducer[] = ENV !== "production" ? [logger
StarkSettingsModule.forRoot(),
StarkRoutingModule.forRoot(),
StarkUserModule.forRoot(),
+ StarkXSRFModule.forRoot({
+ waitBeforePinging: {
+ waitBeforePingingFn: getXsrfWaitBeforePinging,
+ deps: [STARK_SESSION_SERVICE]
+ }
+ }),
SharedModule,
DemoModule,
NewsModule,