mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge pull request #2076 from atmire/w2p-99039_XSRFTokenGetRequest
XSRF Token to ServerAuthRequestService
This commit is contained in:
@@ -11,6 +11,7 @@ import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import objectContaining = jasmine.objectContaining;
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
describe(`AuthRequestService`, () => {
|
||||
let halService: HALEndpointService;
|
||||
@@ -34,8 +35,8 @@ describe(`AuthRequestService`, () => {
|
||||
super(hes, rs, rdbs);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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(),
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { BrowserAuthRequestService } from './browser-auth-request.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { PostRequest } from '../data/request.models';
|
||||
|
||||
describe(`BrowserAuthRequestService`, () => {
|
||||
let href: string;
|
||||
@@ -16,14 +18,20 @@ describe(`BrowserAuthRequestService`, () => {
|
||||
});
|
||||
|
||||
describe(`createShortLivedTokenRequest`, () => {
|
||||
it(`should return a PostRequest`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.constructor.name).toBe('PostRequest');
|
||||
it(`should return a PostRequest`, (done) => {
|
||||
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||
obs.subscribe((result: PostRequest) => {
|
||||
expect(result.constructor.name).toBe('PostRequest');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return a request with the given href`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.href).toBe(href) ;
|
||||
it(`should return a request with the given href`, (done) => {
|
||||
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||
obs.subscribe((result: PostRequest) => {
|
||||
expect(result.href).toBe(href);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,34 +1,68 @@
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { ServerAuthRequestService } from './server-auth-request.service';
|
||||
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { PostRequest } from '../data/request.models';
|
||||
import {
|
||||
XSRF_REQUEST_HEADER,
|
||||
XSRF_RESPONSE_HEADER
|
||||
} from '../xsrf/xsrf.interceptor';
|
||||
|
||||
describe(`ServerAuthRequestService`, () => {
|
||||
let href: string;
|
||||
let requestService: RequestService;
|
||||
let service: AuthRequestService;
|
||||
let httpClient: HttpClient;
|
||||
let httpResponse: HttpResponse<any>;
|
||||
let halService: HALEndpointService;
|
||||
const mockToken = 'mock-token';
|
||||
|
||||
beforeEach(() => {
|
||||
href = 'https://rest.api/auth/shortlivedtokens';
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2'
|
||||
});
|
||||
service = new ServerAuthRequestService(null, requestService, null);
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.set(XSRF_RESPONSE_HEADER, mockToken);
|
||||
httpResponse = {
|
||||
body: { bar: false },
|
||||
headers: headers,
|
||||
statusText: '200'
|
||||
} as HttpResponse<any>;
|
||||
httpClient = jasmine.createSpyObj('httpClient', {
|
||||
get: observableOf(httpResponse),
|
||||
});
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
'getRootHref': '/api'
|
||||
});
|
||||
service = new ServerAuthRequestService(halService, requestService, null, httpClient);
|
||||
});
|
||||
|
||||
describe(`createShortLivedTokenRequest`, () => {
|
||||
it(`should return a GetRequest`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.constructor.name).toBe('GetRequest');
|
||||
it(`should return a PostRequest`, (done) => {
|
||||
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||
obs.subscribe((result: PostRequest) => {
|
||||
expect(result.constructor.name).toBe('PostRequest');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return a request with the given href`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.href).toBe(href) ;
|
||||
it(`should return a request with the given href`, (done) => {
|
||||
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||
obs.subscribe((result: PostRequest) => {
|
||||
expect(result.href).toBe(href);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should have a responseMsToLive of 2 seconds`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.responseMsToLive).toBe(2 * 1000) ;
|
||||
it(`should return a request with a xsrf header`, (done) => {
|
||||
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||
obs.subscribe((result: PostRequest) => {
|
||||
expect(result.options.headers.get(XSRF_REQUEST_HEADER)).toBe(mockToken);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,9 +1,21 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { GetRequest } from '../data/request.models';
|
||||
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,
|
||||
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
|
||||
@@ -14,23 +26,42 @@ export class ServerAuthRequestService extends AuthRequestService {
|
||||
constructor(
|
||||
halService: HALEndpointService,
|
||||
requestService: RequestService,
|
||||
rdbService: RemoteDataBuildService
|
||||
rdbService: RemoteDataBuildService,
|
||||
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): GetRequest {
|
||||
return Object.assign(new GetRequest(this.requestService.generateRequestId(), href), {
|
||||
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,
|
||||
},
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
16
src/app/core/services/server-xhr.service.ts
Normal file
16
src/app/core/services/server-xhr.service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { XhrFactory } from '@angular/common';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { prototype, XMLHttpRequest } from 'xhr2';
|
||||
|
||||
/**
|
||||
* Overrides the default XhrFactory 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 {
|
||||
prototype._restrictedHeaders.cookie = false;
|
||||
return new XMLHttpRequest();
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user