Retrieve the XSRF token first, and set it as both the XSRF header and cookie

This commit is contained in:
Art Lowel
2023-02-07 14:49:22 +01:00
parent 6d99f51d78
commit 06de559974
6 changed files with 69 additions and 26 deletions

View File

@@ -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<PostRequest>;
/**
* 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<ShortLivedToken>(request.uuid)),
getFirstCompletedRemoteData(),

View File

@@ -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<PostRequest> {
return observableOf(new PostRequest(this.requestService.generateRequestId(), href));
}
}

View File

@@ -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<PostRequest> {
// 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<any>) => 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,
}
))
)
}
}

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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 {