mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Bug fix: Ensure we also look for a change in XSRF token during error responses.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||||
import { HttpHeaders, HTTP_INTERCEPTORS, HttpResponse, HttpXsrfTokenExtractor } from '@angular/common/http';
|
import { HttpHeaders, HTTP_INTERCEPTORS, HttpResponse, HttpXsrfTokenExtractor, HttpErrorResponse } from '@angular/common/http';
|
||||||
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
|
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
import { CookieService } from '../services/cookie.service';
|
import { CookieService } from '../services/cookie.service';
|
||||||
@@ -148,4 +148,38 @@ describe(`XsrfInterceptor`, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update XSRF-TOKEN cookie when DSPACE-XSRF-TOKEN header found in error response', (done) => {
|
||||||
|
// Create a mock XSRF token to be returned in response within DSPACE-XSRF-TOKEN header
|
||||||
|
// In this situation, we are mocking a CSRF token mismatch, which causes our backend to send a new token
|
||||||
|
const mockNewXSRFToken = '987654321zyxwut';
|
||||||
|
const mockErrorCode = 403;
|
||||||
|
const mockErrorText = 'Forbidden';
|
||||||
|
const mockErrorMessage = 'CSRF token mismatch';
|
||||||
|
|
||||||
|
service.request(RestRequestMethod.GET, 'server/api/core/items').subscribe({
|
||||||
|
error: (error) => {
|
||||||
|
expect(error).toBeTruthy();
|
||||||
|
|
||||||
|
// ensure mock error (added in below flush() call) is returned.
|
||||||
|
expect(error.statusCode).toBe(mockErrorCode);
|
||||||
|
expect(error.statusText).toBe(mockErrorText);
|
||||||
|
|
||||||
|
// ensure our XSRF-TOKEN cookie exists & has the same value as the new DSPACE-XSRF-TOKEN header
|
||||||
|
expect(cookieService.get('XSRF-TOKEN')).toBeDefined();
|
||||||
|
expect(cookieService.get('XSRF-TOKEN')).toBe(mockNewXSRFToken.toString());
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const httpRequest = httpMock.expectOne('server/api/core/items');
|
||||||
|
|
||||||
|
// Flush & create mock error response (including sending back a new XSRF token in header)
|
||||||
|
httpRequest.flush(mockErrorMessage, {
|
||||||
|
headers: new HttpHeaders().set('DSPACE-XSRF-TOKEN', mockNewXSRFToken),
|
||||||
|
status: mockErrorCode,
|
||||||
|
statusText: mockErrorText
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse, HttpXsrfTokenExtractor } from '@angular/common/http';
|
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse, HttpXsrfTokenExtractor } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { tap, filter } from 'rxjs/operators';
|
import { tap, catchError } from 'rxjs/operators';
|
||||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||||
import { CookieService } from '../services/cookie.service';
|
import { CookieService } from '../services/cookie.service';
|
||||||
|
import { throwError } from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Http Interceptor intercepting Http Requests & Responses to
|
* Custom Http Interceptor intercepting Http Requests & Responses to
|
||||||
@@ -42,6 +43,11 @@ export class XsrfInterceptor implements HttpInterceptor {
|
|||||||
* @param next
|
* @param next
|
||||||
*/
|
*/
|
||||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
// Name of XSRF header we may send in requests to backend (this is a standard name defined by Angular)
|
||||||
|
const requestCsrfHeader = 'X-XSRF-TOKEN';
|
||||||
|
// Name of XSRF header we may receive in responses from backend
|
||||||
|
const responseCsrfHeader = 'DSPACE-XSRF-TOKEN';
|
||||||
|
|
||||||
// Ensure EVERY request from Angular includes "withCredentials: true".
|
// Ensure EVERY request from Angular includes "withCredentials: true".
|
||||||
// This allows Angular to receive & send cookies via a CORS request (to
|
// This allows Angular to receive & send cookies via a CORS request (to
|
||||||
// the backend). ONLY requests with credentials will:
|
// the backend). ONLY requests with credentials will:
|
||||||
@@ -65,29 +71,47 @@ export class XsrfInterceptor implements HttpInterceptor {
|
|||||||
const token = this.tokenExtractor.getToken() as string;
|
const token = this.tokenExtractor.getToken() as string;
|
||||||
|
|
||||||
// send token in request's X-XSRF-TOKEN header (anti-CSRF security) to backend
|
// send token in request's X-XSRF-TOKEN header (anti-CSRF security) to backend
|
||||||
const headerName = 'X-XSRF-TOKEN';
|
if (token !== null && !req.headers.has(requestCsrfHeader)) {
|
||||||
if (token !== null && !req.headers.has(headerName)) {
|
req = req.clone({ headers: req.headers.set(requestCsrfHeader, token) });
|
||||||
req = req.clone({ headers: req.headers.set(headerName, token) });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Pass to next interceptor, but intercept EVERY response event as well
|
// Pass to next interceptor, but intercept EVERY response event as well
|
||||||
return next.handle(req).pipe(
|
return next.handle(req).pipe(
|
||||||
// Check event that came back...is it an HttpResponse from backend?
|
// Check event that came back...is it an HttpResponse from backend?
|
||||||
filter((event) => event instanceof HttpResponse),
|
tap((response) => {
|
||||||
tap((response: HttpResponse<any>) => {
|
if (response instanceof HttpResponse) {
|
||||||
// For every response that comes back, check for the custom
|
// For every response that comes back, check for the custom
|
||||||
// DSPACE-XSRF-TOKEN header sent from the backend.
|
// DSPACE-XSRF-TOKEN header sent from the backend.
|
||||||
if (response.headers.has('DSPACE-XSRF-TOKEN')) {
|
if (response.headers.has(responseCsrfHeader)) {
|
||||||
// value of header is a new XSRF token
|
// value of header is a new XSRF token
|
||||||
const newToken = response.headers.get('DSPACE-XSRF-TOKEN');
|
this.saveXsrfToken(response.headers.get(responseCsrfHeader));
|
||||||
|
|
||||||
// save token value as a *new* value of our client-side
|
|
||||||
// XSRF-TOKEN cookie. (This is the same cookie we use to
|
|
||||||
// send back the X-XSRF-TOKEN header. See request logic above)
|
|
||||||
this.cookieService.remove('XSRF-TOKEN');
|
|
||||||
this.cookieService.set('XSRF-TOKEN', newToken);
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
if (error instanceof HttpErrorResponse) {
|
||||||
|
// For every error that comes back, also check for the custom
|
||||||
|
// DSPACE-XSRF-TOKEN header sent from the backend.
|
||||||
|
if (error.headers.has(responseCsrfHeader)) {
|
||||||
|
// value of header is a new XSRF token
|
||||||
|
this.saveXsrfToken(error.headers.get(responseCsrfHeader));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return error response as is.
|
||||||
|
return throwError(error);
|
||||||
|
})
|
||||||
) as any;
|
) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save XSRF token found in response
|
||||||
|
* @param token token found
|
||||||
|
*/
|
||||||
|
private saveXsrfToken(token: string) {
|
||||||
|
// Save token value as a *new* value of our client-side XSRF-TOKEN cookie.
|
||||||
|
// This is the cookie that is parsed by Angular's tokenExtractor(),
|
||||||
|
// which we will send back in the X-XSRF-TOKEN header per Angular best practices.
|
||||||
|
this.cookieService.remove('XSRF-TOKEN');
|
||||||
|
this.cookieService.set('XSRF-TOKEN', token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user