diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 5c0c3340c7..7c1f17dec2 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -100,14 +100,12 @@ export abstract class AuthRequestService { ); } /** - * Factory function to create the request object to send. This needs to be a POST client side and - * a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow - * only the server IP to send a GET to this endpoint. + * Factory function to create the request object to send. * * @param href The href to send the request to * @protected */ - protected abstract createShortLivedTokenRequest(href: string): GetRequest | PostRequest; + protected abstract createShortLivedTokenRequest(href: string): Observable; /** * Send a request to retrieve a short-lived token which provides download access of restricted files @@ -117,7 +115,7 @@ export abstract class AuthRequestService { filter((href: string) => isNotEmpty(href)), distinctUntilChanged(), map((href: string) => new URLCombiner(href, this.shortlivedtokensEndpoint).toString()), - map((endpointURL: string) => this.createShortLivedTokenRequest(endpointURL)), + switchMap((endpointURL: string) => this.createShortLivedTokenRequest(endpointURL)), tap((request: RestRequest) => this.requestService.send(request)), switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid)), getFirstCompletedRemoteData(), diff --git a/src/app/core/auth/browser-auth-request.service.ts b/src/app/core/auth/browser-auth-request.service.ts index 85d5f54340..485e2ef9c4 100644 --- a/src/app/core/auth/browser-auth-request.service.ts +++ b/src/app/core/auth/browser-auth-request.service.ts @@ -4,6 +4,7 @@ import { PostRequest } from '../data/request.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { Observable, of as observableOf } from 'rxjs'; /** * Client side version of the service to send authentication requests @@ -20,15 +21,13 @@ export class BrowserAuthRequestService extends AuthRequestService { } /** - * Factory function to create the request object to send. This needs to be a POST client side and - * a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow - * only the server IP to send a GET to this endpoint. + * Factory function to create the request object to send. * * @param href The href to send the request to * @protected */ - protected createShortLivedTokenRequest(href: string): PostRequest { - return new PostRequest(this.requestService.generateRequestId(), href); + protected createShortLivedTokenRequest(href: string): Observable { + return observableOf(new PostRequest(this.requestService.generateRequestId(), href)); } } diff --git a/src/app/core/auth/server-auth-request.service.ts b/src/app/core/auth/server-auth-request.service.ts index 9954e7c7ef..05131b7375 100644 --- a/src/app/core/auth/server-auth-request.service.ts +++ b/src/app/core/auth/server-auth-request.service.ts @@ -4,8 +4,19 @@ import { PostRequest } from '../data/request.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { HttpHeaders, HttpXsrfTokenExtractor } from '@angular/common/http'; -import { XSRF_REQUEST_HEADER } from '../xsrf/xsrf.interceptor'; +import { + HttpHeaders, + HttpXsrfTokenExtractor, + HttpClient, + HttpResponse +} from '@angular/common/http'; +import { + XSRF_REQUEST_HEADER, + XSRF_RESPONSE_HEADER, + DSPACE_XSRF_COOKIE +} from '../xsrf/xsrf.interceptor'; +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; /** * Server side version of the service to send authentication requests @@ -17,29 +28,40 @@ export class ServerAuthRequestService extends AuthRequestService { halService: HALEndpointService, requestService: RequestService, rdbService: RemoteDataBuildService, - protected tokenExtractor: HttpXsrfTokenExtractor, + protected httpClient: HttpClient, ) { super(halService, requestService, rdbService); } /** - * Factory function to create the request object to send. This needs to be a POST client side and - * a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow - * only the server IP to send a GET to this endpoint. + * Factory function to create the request object to send. * * @param href The href to send the request to * @protected */ - protected createShortLivedTokenRequest(href: string): PostRequest { - let options = new HttpHeaders(); - options = options.set('Content-Type', 'application/json; charset=utf-8'); - options = options.set(XSRF_REQUEST_HEADER, this.tokenExtractor.getToken()); - let requestOptions = { - headers: options, - }; - return Object.assign(new PostRequest(this.requestService.generateRequestId(), href, {}, requestOptions), { - responseMsToLive: 2 * 1000 // A short lived token is only valid for 2 seconds. - }); + protected createShortLivedTokenRequest(href: string): Observable { + // First do a call to the root endpoint in order to get an XSRF token + return this.httpClient.get(this.halService.getRootHref(), { observe: 'response' }).pipe( + // retrieve the XSRF token from the response header + map((response: HttpResponse) => response.headers.get(XSRF_RESPONSE_HEADER)), + // Use that token to create an HttpHeaders object + map((xsrfToken: string) => new HttpHeaders() + .set('Content-Type', 'application/json; charset=utf-8') + // set the token as the XSRF header + .set(XSRF_REQUEST_HEADER, xsrfToken) + // and as the DSPACE-XSRF-COOKIE + .set('Cookie', `${DSPACE_XSRF_COOKIE}=${xsrfToken}`)), + map((headers: HttpHeaders) => + // Create a new PostRequest using those headers and the given href + new PostRequest( + this.requestService.generateRequestId(), + href, + {}, + { + headers: headers, + } + )) + ) } } diff --git a/src/app/core/services/server-xhr.service.ts b/src/app/core/services/server-xhr.service.ts new file mode 100644 index 0000000000..835072bbd3 --- /dev/null +++ b/src/app/core/services/server-xhr.service.ts @@ -0,0 +1,16 @@ +import { XhrFactory } from '@angular/common'; +import { Injectable } from '@angular/core'; +import * as xhr2 from 'xhr2'; + +/** + * Overrides the default XhrFactoru server side, to allow us to set cookies in requests to the + * backend. This was added to be able to perform a working XSRF request from the node server, as it + * needs to set a cookie for the XSRF token + */ +@Injectable() +export class ServerXhrService implements XhrFactory { + build(): XMLHttpRequest { + xhr2.prototype._restrictedHeaders.cookie = false; + return new xhr2.XMLHttpRequest(); + } +} diff --git a/src/app/core/xsrf/xsrf.interceptor.ts b/src/app/core/xsrf/xsrf.interceptor.ts index d527924a28..cded432397 100644 --- a/src/app/core/xsrf/xsrf.interceptor.ts +++ b/src/app/core/xsrf/xsrf.interceptor.ts @@ -19,6 +19,8 @@ export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN'; export const XSRF_RESPONSE_HEADER = 'DSPACE-XSRF-TOKEN'; // Name of cookie where we store the XSRF token export const XSRF_COOKIE = 'XSRF-TOKEN'; +// Name of cookie the backend expects the XSRF token to be in +export const DSPACE_XSRF_COOKIE = 'DSPACE-XSRF-COOKIE'; /** * Custom Http Interceptor intercepting Http Requests & Responses to diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts index 81426e7fcc..7d162c5fd1 100644 --- a/src/modules/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -33,6 +33,8 @@ import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mo import { AuthRequestService } from '../../app/core/auth/auth-request.service'; import { ServerAuthRequestService } from '../../app/core/auth/server-auth-request.service'; import { ServerInitService } from './server-init.service'; +import { XhrFactory } from '@angular/common'; +import { ServerXhrService } from '../../app/core/services/server-xhr.service'; export function createTranslateLoader(transferState: TransferState) { return new TranslateServerLoader(transferState, 'dist/server/assets/i18n/', '.json'); @@ -104,6 +106,10 @@ export function createTranslateLoader(transferState: TransferState) { provide: HardRedirectService, useClass: ServerHardRedirectService, }, + { + provide: XhrFactory, + useClass: ServerXhrService, + }, ] }) export class ServerAppModule {