mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
make a call to ensure a correct XSRF token before performing any non-GET requests
This commit is contained in:
@@ -53,6 +53,7 @@ import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
|
|||||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
import { UUIDService } from '../../../core/shared/uuid.service';
|
import { UUIDService } from '../../../core/shared/uuid.service';
|
||||||
|
import { XSRFService } from '../../../core/xsrf/xsrf.service';
|
||||||
import { AlertComponent } from '../../../shared/alert/alert.component';
|
import { AlertComponent } from '../../../shared/alert/alert.component';
|
||||||
import { ContextHelpDirective } from '../../../shared/context-help.directive';
|
import { ContextHelpDirective } from '../../../shared/context-help.directive';
|
||||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||||
@@ -244,6 +245,7 @@ describe('GroupFormComponent', () => {
|
|||||||
{ provide: HttpClient, useValue: {} },
|
{ provide: HttpClient, useValue: {} },
|
||||||
{ provide: ObjectCacheService, useValue: {} },
|
{ provide: ObjectCacheService, useValue: {} },
|
||||||
{ provide: UUIDService, useValue: {} },
|
{ provide: UUIDService, useValue: {} },
|
||||||
|
{ provide: XSRFService, useValue: {} },
|
||||||
{ provide: Store, useValue: {} },
|
{ provide: Store, useValue: {} },
|
||||||
{ provide: RemoteDataBuildService, useValue: {} },
|
{ provide: RemoteDataBuildService, useValue: {} },
|
||||||
{ provide: HALEndpointService, useValue: {} },
|
{ provide: HALEndpointService, useValue: {} },
|
||||||
|
@@ -16,6 +16,7 @@ import {
|
|||||||
getTestScheduler,
|
getTestScheduler,
|
||||||
} from 'jasmine-marbles';
|
} from 'jasmine-marbles';
|
||||||
import {
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
EMPTY,
|
EMPTY,
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
@@ -32,6 +33,7 @@ import { ObjectCacheService } from '../cache/object-cache.service';
|
|||||||
import { coreReducers } from '../core.reducers';
|
import { coreReducers } from '../core.reducers';
|
||||||
import { CoreState } from '../core-state.model';
|
import { CoreState } from '../core-state.model';
|
||||||
import { UUIDService } from '../shared/uuid.service';
|
import { UUIDService } from '../shared/uuid.service';
|
||||||
|
import { XSRFService } from '../xsrf/xsrf.service';
|
||||||
import {
|
import {
|
||||||
RequestConfigureAction,
|
RequestConfigureAction,
|
||||||
RequestExecuteAction,
|
RequestExecuteAction,
|
||||||
@@ -59,6 +61,7 @@ describe('RequestService', () => {
|
|||||||
let uuidService: UUIDService;
|
let uuidService: UUIDService;
|
||||||
let store: Store<CoreState>;
|
let store: Store<CoreState>;
|
||||||
let mockStore: MockStore<CoreState>;
|
let mockStore: MockStore<CoreState>;
|
||||||
|
let xsrfService: XSRFService;
|
||||||
|
|
||||||
const testUUID = '5f2a0d2a-effa-4d54-bd54-5663b960f9eb';
|
const testUUID = '5f2a0d2a-effa-4d54-bd54-5663b960f9eb';
|
||||||
const testHref = 'https://rest.api/endpoint/selfLink';
|
const testHref = 'https://rest.api/endpoint/selfLink';
|
||||||
@@ -104,10 +107,15 @@ describe('RequestService', () => {
|
|||||||
store = TestBed.inject(Store);
|
store = TestBed.inject(Store);
|
||||||
mockStore = store as MockStore<CoreState>;
|
mockStore = store as MockStore<CoreState>;
|
||||||
mockStore.setState(initialState);
|
mockStore.setState(initialState);
|
||||||
|
xsrfService = {
|
||||||
|
tokenInitialized$: new BehaviorSubject(false),
|
||||||
|
} as XSRFService;
|
||||||
|
|
||||||
service = new RequestService(
|
service = new RequestService(
|
||||||
objectCache,
|
objectCache,
|
||||||
uuidService,
|
uuidService,
|
||||||
store,
|
store,
|
||||||
|
xsrfService,
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
serviceAsAny = service as any;
|
serviceAsAny = service as any;
|
||||||
|
@@ -42,6 +42,7 @@ import {
|
|||||||
requestIndexSelector,
|
requestIndexSelector,
|
||||||
} from '../index/index.selectors';
|
} from '../index/index.selectors';
|
||||||
import { UUIDService } from '../shared/uuid.service';
|
import { UUIDService } from '../shared/uuid.service';
|
||||||
|
import { XSRFService } from '../xsrf/xsrf.service';
|
||||||
import {
|
import {
|
||||||
RequestConfigureAction,
|
RequestConfigureAction,
|
||||||
RequestExecuteAction,
|
RequestExecuteAction,
|
||||||
@@ -168,6 +169,7 @@ export class RequestService {
|
|||||||
constructor(private objectCache: ObjectCacheService,
|
constructor(private objectCache: ObjectCacheService,
|
||||||
private uuidService: UUIDService,
|
private uuidService: UUIDService,
|
||||||
private store: Store<CoreState>,
|
private store: Store<CoreState>,
|
||||||
|
protected xsrfService: XSRFService,
|
||||||
private indexStore: Store<MetaIndexState>) {
|
private indexStore: Store<MetaIndexState>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,7 +452,17 @@ export class RequestService {
|
|||||||
*/
|
*/
|
||||||
private dispatchRequest(request: RestRequest) {
|
private dispatchRequest(request: RestRequest) {
|
||||||
this.store.dispatch(new RequestConfigureAction(request));
|
this.store.dispatch(new RequestConfigureAction(request));
|
||||||
this.store.dispatch(new RequestExecuteAction(request.uuid));
|
// If it's a GET request, or we have an XSRF token, dispatch it immediately
|
||||||
|
if (request.method === RestRequestMethod.GET || this.xsrfService.tokenInitialized$.getValue() === true) {
|
||||||
|
this.store.dispatch(new RequestExecuteAction(request.uuid));
|
||||||
|
} else {
|
||||||
|
// Otherwise wait for the XSRF token first
|
||||||
|
this.xsrfService.tokenInitialized$.pipe(
|
||||||
|
find((hasInitialized: boolean) => hasInitialized === true),
|
||||||
|
).subscribe(() => {
|
||||||
|
this.store.dispatch(new RequestExecuteAction(request.uuid));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
64
src/app/core/xsrf/browser-xsrf.service.spec.ts
Normal file
64
src/app/core/xsrf/browser-xsrf.service.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { BrowserXSRFService } from './browser-xsrf.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||||
|
|
||||||
|
describe(`BrowserXSRFService`, () => {
|
||||||
|
let service: BrowserXSRFService;
|
||||||
|
let httpClient: HttpClient;
|
||||||
|
let httpTestingController: HttpTestingController;
|
||||||
|
|
||||||
|
const endpointURL = new RESTURLCombiner('/security/csrf').toString();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ HttpClientTestingModule ],
|
||||||
|
providers: [ BrowserXSRFService ]
|
||||||
|
});
|
||||||
|
httpClient = TestBed.inject(HttpClient);
|
||||||
|
httpTestingController = TestBed.inject(HttpTestingController);
|
||||||
|
service = TestBed.inject(BrowserXSRFService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`initXSRFToken`, () => {
|
||||||
|
it(`should perform a POST to the csrf endpoint`, () => {
|
||||||
|
service.initXSRFToken(httpClient)();
|
||||||
|
|
||||||
|
const req = httpTestingController.expectOne({
|
||||||
|
url: endpointURL,
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
req.flush({});
|
||||||
|
httpTestingController.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`when the POST succeeds`, () => {
|
||||||
|
it(`should set tokenInitialized$ to true`, () => {
|
||||||
|
service.initXSRFToken(httpClient)();
|
||||||
|
|
||||||
|
const req = httpTestingController.expectOne(endpointURL);
|
||||||
|
|
||||||
|
req.flush({});
|
||||||
|
httpTestingController.verify();
|
||||||
|
|
||||||
|
expect(service.tokenInitialized$.getValue()).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`when the POST fails`, () => {
|
||||||
|
it(`should set tokenInitialized$ to true`, () => {
|
||||||
|
service.initXSRFToken(httpClient)();
|
||||||
|
|
||||||
|
const req = httpTestingController.expectOne(endpointURL);
|
||||||
|
|
||||||
|
req.error(new ErrorEvent('415'));
|
||||||
|
httpTestingController.verify();
|
||||||
|
|
||||||
|
expect(service.tokenInitialized$.getValue()).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
26
src/app/core/xsrf/browser-xsrf.service.ts
Normal file
26
src/app/core/xsrf/browser-xsrf.service.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||||
|
import { take, catchError } from 'rxjs/operators';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { XSRFService } from './xsrf.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BrowserXSRFService extends XSRFService {
|
||||||
|
initXSRFToken(httpClient: HttpClient): () => Promise<any> {
|
||||||
|
return () => new Promise((resolve) => {
|
||||||
|
httpClient.post(new RESTURLCombiner('/security/csrf').toString(), undefined).pipe(
|
||||||
|
// errors are to be expected if the token and the cookie don't match, that's what we're
|
||||||
|
// trying to fix for future requests, so just emit any observable to end up in the
|
||||||
|
// subscribe
|
||||||
|
catchError(() => observableOf(null)),
|
||||||
|
take(1),
|
||||||
|
).subscribe(() => {
|
||||||
|
this.tokenInitialized$.next(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// return immediately, the rest of the app doesn't need to wait for this to finish
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
32
src/app/core/xsrf/server-xsrf.service.spec.ts
Normal file
32
src/app/core/xsrf/server-xsrf.service.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ServerXSRFService } from './server-xsrf.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
|
describe(`ServerXSRFService`, () => {
|
||||||
|
let service: ServerXSRFService;
|
||||||
|
let httpClient: HttpClient;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
httpClient = jasmine.createSpyObj(['post', 'get', 'request']);
|
||||||
|
service = new ServerXSRFService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`initXSRFToken`, () => {
|
||||||
|
it(`shouldn't perform any requests`, (done: DoneFn) => {
|
||||||
|
service.initXSRFToken(httpClient)().then(() => {
|
||||||
|
for (const prop in httpClient) {
|
||||||
|
if (httpClient.hasOwnProperty(prop)) {
|
||||||
|
expect(httpClient[prop]).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should leave tokenInitialized$ on false`, (done: DoneFn) => {
|
||||||
|
service.initXSRFToken(httpClient)().then(() => {
|
||||||
|
expect(service.tokenInitialized$.getValue()).toBeFalse();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
14
src/app/core/xsrf/server-xsrf.service.ts
Normal file
14
src/app/core/xsrf/server-xsrf.service.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { XSRFService } from './xsrf.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ServerXSRFService extends XSRFService {
|
||||||
|
initXSRFToken(httpClient: HttpClient): () => Promise<any> {
|
||||||
|
return () => new Promise((resolve) => {
|
||||||
|
// return immediately, and keep tokenInitialized$ false. The server side can make only GET
|
||||||
|
// requests, since it can never get a valid XSRF cookie
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
20
src/app/core/xsrf/xsrf.service.spec.ts
Normal file
20
src/app/core/xsrf/xsrf.service.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { XSRFService } from './xsrf.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
|
class XSRFServiceImpl extends XSRFService {
|
||||||
|
initXSRFToken(httpClient: HttpClient): () => Promise<any> {
|
||||||
|
return () => null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe(`XSRFService`, () => {
|
||||||
|
let service: XSRFService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new XSRFServiceImpl();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should start with tokenInitialized$.hasValue() === false`, () => {
|
||||||
|
expect(service.tokenInitialized$.getValue()).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
10
src/app/core/xsrf/xsrf.service.ts
Normal file
10
src/app/core/xsrf/xsrf.service.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export abstract class XSRFService {
|
||||||
|
public tokenInitialized$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
|
|
||||||
|
abstract initXSRFToken(httpClient: HttpClient): () => Promise<any>;
|
||||||
|
}
|
@@ -2,7 +2,10 @@ import {
|
|||||||
HttpClient,
|
HttpClient,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
import { NgModule } from '@angular/core';
|
import {
|
||||||
|
APP_INITIALIZER,
|
||||||
|
NgModule,
|
||||||
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
BrowserTransferStateModule,
|
BrowserTransferStateModule,
|
||||||
@@ -48,13 +51,15 @@ import { ClientCookieService } from '../../app/core/services/client-cookie.servi
|
|||||||
import { CookieService } from '../../app/core/services/cookie.service';
|
import { CookieService } from '../../app/core/services/cookie.service';
|
||||||
import { HardRedirectService } from '../../app/core/services/hard-redirect.service';
|
import { HardRedirectService } from '../../app/core/services/hard-redirect.service';
|
||||||
import { ReferrerService } from '../../app/core/services/referrer.service';
|
import { ReferrerService } from '../../app/core/services/referrer.service';
|
||||||
|
import { BrowserXSRFService } from '../../app/core/xsrf/browser-xsrf.service';
|
||||||
|
import { XSRFService } from '../../app/core/xsrf/xsrf.service';
|
||||||
import { BrowserKlaroService } from '../../app/shared/cookies/browser-klaro.service';
|
import { BrowserKlaroService } from '../../app/shared/cookies/browser-klaro.service';
|
||||||
import { KlaroService } from '../../app/shared/cookies/klaro.service';
|
import { KlaroService } from '../../app/shared/cookies/klaro.service';
|
||||||
import { MissingTranslationHelper } from '../../app/shared/translate/missing-translation.helper';
|
import { MissingTranslationHelper } from '../../app/shared/translate/missing-translation.helper';
|
||||||
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
||||||
import { SubmissionService } from '../../app/submission/submission.service';
|
import { SubmissionService } from '../../app/submission/submission.service';
|
||||||
import { TranslateBrowserLoader } from '../../ngx-translate-loaders/translate-browser.loader';
|
import { TranslateBrowserLoader } from '../../ngx-translate-loaders/translate-browser.loader';
|
||||||
import { BrowserInitService } from './browser-init.service';
|
import { BrowserInitService } from './browser-init.service'
|
||||||
|
|
||||||
export const REQ_KEY = makeStateKey<string>('req');
|
export const REQ_KEY = makeStateKey<string>('req');
|
||||||
|
|
||||||
@@ -98,6 +103,16 @@ export function getRequest(transferState: TransferState): any {
|
|||||||
useFactory: getRequest,
|
useFactory: getRequest,
|
||||||
deps: [TransferState],
|
deps: [TransferState],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
useFactory: (xsrfService: XSRFService, httpClient: HttpClient) => xsrfService.initXSRFToken(httpClient),
|
||||||
|
deps: [ XSRFService, HttpClient ],
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: XSRFService,
|
||||||
|
useClass: BrowserXSRFService,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: AuthService,
|
provide: AuthService,
|
||||||
useClass: AuthService,
|
useClass: AuthService,
|
||||||
|
@@ -46,6 +46,8 @@ import { ServerReferrerService } from '../../app/core/services/server.referrer.s
|
|||||||
import { ServerCookieService } from '../../app/core/services/server-cookie.service';
|
import { ServerCookieService } from '../../app/core/services/server-cookie.service';
|
||||||
import { ServerHardRedirectService } from '../../app/core/services/server-hard-redirect.service';
|
import { ServerHardRedirectService } from '../../app/core/services/server-hard-redirect.service';
|
||||||
import { ServerXhrService } from '../../app/core/services/server-xhr.service';
|
import { ServerXhrService } from '../../app/core/services/server-xhr.service';
|
||||||
|
import { ServerXSRFService } from '../../app/core/xsrf/server-xsrf.service';
|
||||||
|
import { XSRFService } from '../../app/core/xsrf/xsrf.service';
|
||||||
import { AngularticsProviderMock } from '../../app/shared/mocks/angulartics-provider.service.mock';
|
import { AngularticsProviderMock } from '../../app/shared/mocks/angulartics-provider.service.mock';
|
||||||
import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mock';
|
import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mock';
|
||||||
import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider';
|
import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider';
|
||||||
@@ -112,6 +114,10 @@ export function createTranslateLoader(transferState: TransferState) {
|
|||||||
provide: AuthRequestService,
|
provide: AuthRequestService,
|
||||||
useClass: ServerAuthRequestService,
|
useClass: ServerAuthRequestService,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: XSRFService,
|
||||||
|
useClass: ServerXSRFService,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: LocaleService,
|
provide: LocaleService,
|
||||||
useClass: ServerLocaleService,
|
useClass: ServerLocaleService,
|
||||||
|
Reference in New Issue
Block a user