Files
dspace-angular/src/app/core/xsrf/xsrf.interceptor.ts
Yury Bondarenko c0f43bc585 ESLint: fix rxjs/no-implicit-any-catch
In most cases we can deal with the untyped errors by introducing an explicit instanceof check

DspaceRestService includes an unsafe catch/rethrow → made it explicitly typed as `any`, but it should be revisited at some point
2023-06-28 16:30:38 +02:00

132 lines
5.2 KiB
TypeScript

import {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpResponse,
HttpXsrfTokenExtractor,
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import {
Observable,
throwError,
} from 'rxjs';
import {
catchError,
tap,
} from 'rxjs/operators';
import { CookieService } from '../services/cookie.service';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
import {
XSRF_COOKIE,
XSRF_REQUEST_HEADER,
XSRF_RESPONSE_HEADER,
} from './xsrf.constants';
/**
* Custom Http Interceptor intercepting Http Requests & Responses to
* exchange XSRF/CSRF tokens with the backend.
*
* DSpace has a custom XSRF token process in order to support the UI and backend
* running on entirely separate domains:
*
* 1. Backend generates XSRF token & stores in a *server-side* (only) cookie
* named DSPACE-XSRF-COOKIE. This cookie is not readable to Angular, but is
* returned (by user's browser) on every subsequent request to backend.
* 2. Backend also sends XSRF token in a header named DSPACE-XSRF-TOKEN to
* Angular.
* 3. This interceptor looks for DSPACE-XSRF-TOKEN header in a response. If
* found, its value is saved to a *client-side* (only) cookie named XSRF-TOKEN.
* 4. Whenever Angular is making a mutating request (POST, PUT, DELETE, etc),
* this interceptor checks for that client-side XSRF-TOKEN cookie. If found,
* its value is sent to the backend in the X-XSRF-TOKEN header.
* 5. On backend, the X-XSRF-TOKEN header is received & compared to the current
* value of the *server-side* cookie named DSPACE-XSRF-COOKIE. If tokens
* match, the request is accepted. If tokens don't match a 403 is returned.
*
* In summary, the XSRF token is ALWAYS sent to/from the Angular UI and backend
* via *headers*. Both the Angular UI and backend have cookies which retain the
* last known value of the XSRF token for verification purposes.
*/
@Injectable()
export class XsrfInterceptor implements HttpInterceptor {
constructor(private tokenExtractor: HttpXsrfTokenExtractor, private cookieService: CookieService) {
}
/**
* Intercept http requests and add the XSRF/CSRF token to the X-Forwarded-For header
* @param httpRequest
* @param next
*/
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Ensure EVERY request from Angular includes "withCredentials: true".
// This allows Angular to receive & send cookies via a CORS request (to
// the backend). ONLY requests with credentials will:
// 1. Ensure a user's browser sends the server-side XSRF cookie back to
// the backend
// 2. Ensure a user's browser saves changes to the server-side XSRF
// cookie (to ensure it is kept in sync with client side cookie)
req = req.clone({ withCredentials: true });
// Get request URL
const reqUrl = req.url.toLowerCase();
// Get root URL of configured REST API
const restUrl = new RESTURLCombiner('/').toString().toLowerCase();
// Skip any non-mutating request. This is because our REST API does NOT
// require CSRF verification for read-only requests like GET or HEAD
// Also skip any request which is NOT to our trusted/configured REST API
if (req.method !== 'GET' && req.method !== 'HEAD' && reqUrl.startsWith(restUrl)) {
// parse token from XSRF-TOKEN (client-side) cookie
const token = this.tokenExtractor.getToken() as string;
// send token in request's X-XSRF-TOKEN header (anti-CSRF security) to backend
if (token !== null && !req.headers.has(XSRF_REQUEST_HEADER)) {
req = req.clone({ headers: req.headers.set(XSRF_REQUEST_HEADER, token) });
}
}
// Pass to next interceptor, but intercept EVERY response event as well
return next.handle(req).pipe(
// Check event that came back...is it an HttpResponse from backend?
tap((response) => {
if (response instanceof HttpResponse) {
// For every response that comes back, check for the custom
// DSPACE-XSRF-TOKEN header sent from the backend.
if (response.headers.has(XSRF_RESPONSE_HEADER)) {
// value of header is a new XSRF token
this.saveXsrfToken(response.headers.get(XSRF_RESPONSE_HEADER));
}
}
}),
catchError((error: unknown) => {
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(XSRF_RESPONSE_HEADER)) {
// value of header is a new XSRF token
this.saveXsrfToken(error.headers.get(XSRF_RESPONSE_HEADER));
}
}
// Return error response as is.
return throwError(error);
}),
) 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_COOKIE);
this.cookieService.set(XSRF_COOKIE, token);
}
}