mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 02:24:11 +00:00
use GET for shortlivedtoken requests on the server, POST on the client
This commit is contained in:
74
src/app/core/auth/auth-request.service.spec.ts
Normal file
74
src/app/core/auth/auth-request.service.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { PostRequest } from '../data/request.models';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { ShortLivedToken } from './models/short-lived-token.model';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
|
||||
describe(`AuthRequestService`, () => {
|
||||
let halService: HALEndpointService;
|
||||
let endpointURL: string;
|
||||
let shortLivedToken: ShortLivedToken;
|
||||
let shortLivedTokenRD: RemoteData<ShortLivedToken>;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let service: AuthRequestService;
|
||||
let testScheduler;
|
||||
|
||||
class TestAuthRequestService extends AuthRequestService {
|
||||
constructor(
|
||||
hes: HALEndpointService,
|
||||
rs: RequestService,
|
||||
rdbs: RemoteDataBuildService
|
||||
) {
|
||||
super(hes, rs, rdbs);
|
||||
}
|
||||
|
||||
protected createShortLivedTokenRequest(href: string): PostRequest {
|
||||
return new PostRequest(this.requestService.generateRequestId(), href);
|
||||
}
|
||||
}
|
||||
|
||||
const init = (cold: typeof TestScheduler.prototype.createColdObservable) => {
|
||||
endpointURL = 'https://rest.api/auth';
|
||||
shortLivedToken = Object.assign(new ShortLivedToken(), {
|
||||
value: 'some-token'
|
||||
});
|
||||
shortLivedTokenRD = createSuccessfulRemoteDataObject(shortLivedToken);
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
'getEndpoint': cold('a', { a: endpointURL })
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'send': null
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
'buildFromRequestUUID': cold('a', { a: shortLivedTokenRD })
|
||||
});
|
||||
|
||||
service = new TestAuthRequestService(halService, requestService, rdbService);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
testScheduler = new TestScheduler((actual, expected) => {
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`getShortlivedToken`, () => {
|
||||
it(`should call createShortLivedTokenRequest with the url for the endpoint`, () => {
|
||||
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||
init(cold);
|
||||
spyOn(service as any, 'createShortLivedTokenRequest');
|
||||
// expectObservable is needed to let testScheduler know to take it in to account, but since
|
||||
// we're not testing the outcome in this test, a .toBe(…) isn't necessary
|
||||
expectObservable(service.getShortlivedToken());
|
||||
flush();
|
||||
expect((service as any).createShortLivedTokenRequest).toHaveBeenCalledWith(`${endpointURL}/shortlivedtokens`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,14 +1,9 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import {
|
||||
GetRequest,
|
||||
PostRequest,
|
||||
RestRequest,
|
||||
} from '../data/request.models';
|
||||
import { GetRequest, PostRequest, RestRequest, } from '../data/request.models';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
@@ -17,8 +12,10 @@ import { AuthStatus } from './models/auth-status.model';
|
||||
import { ShortLivedToken } from './models/short-lived-token.model';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
|
||||
@Injectable()
|
||||
export class AuthRequestService {
|
||||
/**
|
||||
* Abstract service to send authentication requests
|
||||
*/
|
||||
export abstract class AuthRequestService {
|
||||
protected linkName = 'authn';
|
||||
protected browseEndpoint = '';
|
||||
protected shortlivedtokensEndpoint = 'shortlivedtokens';
|
||||
@@ -62,16 +59,26 @@ export class AuthRequestService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a POST request to retrieve a short-lived token which provides download access of restricted files
|
||||
* 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.
|
||||
*
|
||||
* @param href The href to send the request to
|
||||
* @protected
|
||||
*/
|
||||
protected abstract createShortLivedTokenRequest(href: string): GetRequest | PostRequest;
|
||||
|
||||
/**
|
||||
* Send a request to retrieve a short-lived token which provides download access of restricted files
|
||||
*/
|
||||
public getShortlivedToken(): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkName).pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
distinctUntilChanged(),
|
||||
map((href: string) => new URLCombiner(href, this.shortlivedtokensEndpoint).toString()),
|
||||
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)),
|
||||
tap((request: PostRequest) => this.requestService.send(request)),
|
||||
switchMap((request: PostRequest) => this.rdbService.buildFromRequestUUID<ShortLivedToken>(request.uuid)),
|
||||
map((endpointURL: string) => this.createShortLivedTokenRequest(endpointURL)),
|
||||
tap((request: RestRequest) => this.requestService.send(request)),
|
||||
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<ShortLivedToken>(request.uuid)),
|
||||
getFirstCompletedRemoteData(),
|
||||
map((response: RemoteData<ShortLivedToken>) => {
|
||||
if (response.hasSucceeded) {
|
||||
|
29
src/app/core/auth/browser-auth-request.service.spec.ts
Normal file
29
src/app/core/auth/browser-auth-request.service.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { BrowserAuthRequestService } from './browser-auth-request.service';
|
||||
|
||||
describe(`BrowserAuthRequestService`, () => {
|
||||
let href: string;
|
||||
let requestService: RequestService;
|
||||
let service: AuthRequestService;
|
||||
|
||||
beforeEach(() => {
|
||||
href = 'https://rest.api/auth/shortlivedtokens';
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2'
|
||||
});
|
||||
service = new BrowserAuthRequestService(null, requestService, null);
|
||||
});
|
||||
|
||||
describe(`createShortLivedTokenRequest`, () => {
|
||||
it(`should return a PostRequest`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.constructor.name).toBe('PostRequest');
|
||||
});
|
||||
|
||||
it(`should return a request with the given href`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.href).toBe(href) ;
|
||||
});
|
||||
});
|
||||
});
|
34
src/app/core/auth/browser-auth-request.service.ts
Normal file
34
src/app/core/auth/browser-auth-request.service.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
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';
|
||||
|
||||
/**
|
||||
* Client side version of the service to send authentication requests
|
||||
*/
|
||||
@Injectable()
|
||||
export class BrowserAuthRequestService extends AuthRequestService {
|
||||
|
||||
constructor(
|
||||
halService: HALEndpointService,
|
||||
requestService: RequestService,
|
||||
rdbService: RemoteDataBuildService
|
||||
) {
|
||||
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.
|
||||
*
|
||||
* @param href The href to send the request to
|
||||
* @protected
|
||||
*/
|
||||
protected createShortLivedTokenRequest(href: string): PostRequest {
|
||||
return new PostRequest(this.requestService.generateRequestId(), href);
|
||||
}
|
||||
|
||||
}
|
34
src/app/core/auth/server-auth-request.service.spec.ts
Normal file
34
src/app/core/auth/server-auth-request.service.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { ServerAuthRequestService } from './server-auth-request.service';
|
||||
|
||||
describe(`ServerAuthRequestService`, () => {
|
||||
let href: string;
|
||||
let requestService: RequestService;
|
||||
let service: AuthRequestService;
|
||||
|
||||
beforeEach(() => {
|
||||
href = 'https://rest.api/auth/shortlivedtokens';
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2'
|
||||
});
|
||||
service = new ServerAuthRequestService(null, requestService, null);
|
||||
});
|
||||
|
||||
describe(`createShortLivedTokenRequest`, () => {
|
||||
it(`should return a GetRequest`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.constructor.name).toBe('GetRequest');
|
||||
});
|
||||
|
||||
it(`should return a request with the given href`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.href).toBe(href) ;
|
||||
});
|
||||
|
||||
it(`should have a responseMsToLive of 2 seconds`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.responseMsToLive).toBe(2 * 1000) ;
|
||||
});
|
||||
});
|
||||
});
|
36
src/app/core/auth/server-auth-request.service.ts
Normal file
36
src/app/core/auth/server-auth-request.service.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { GetRequest } 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';
|
||||
|
||||
/**
|
||||
* Server side version of the service to send authentication requests
|
||||
*/
|
||||
@Injectable()
|
||||
export class ServerAuthRequestService extends AuthRequestService {
|
||||
|
||||
constructor(
|
||||
halService: HALEndpointService,
|
||||
requestService: RequestService,
|
||||
rdbService: RemoteDataBuildService
|
||||
) {
|
||||
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.
|
||||
*
|
||||
* @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.
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -31,7 +31,6 @@ import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { UploaderService } from '../shared/uploader/uploader.service';
|
||||
import { SectionFormOperationsService } from '../submission/sections/form/section-form-operations.service';
|
||||
import { AuthRequestService } from './auth/auth-request.service';
|
||||
import { AuthenticatedGuard } from './auth/authenticated.guard';
|
||||
import { AuthStatus } from './auth/models/auth-status.model';
|
||||
import { BrowseService } from './browse/browse.service';
|
||||
@@ -188,7 +187,6 @@ const EXPORTS = [];
|
||||
const PROVIDERS = [
|
||||
ApiService,
|
||||
AuthenticatedGuard,
|
||||
AuthRequestService,
|
||||
CommunityDataService,
|
||||
CollectionDataService,
|
||||
SiteDataService,
|
||||
|
@@ -31,6 +31,8 @@ import {
|
||||
import { LocaleService } from '../../app/core/locale/locale.service';
|
||||
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
||||
import { RouterModule, NoPreloading } from '@angular/router';
|
||||
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
|
||||
import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service';
|
||||
|
||||
export const REQ_KEY = makeStateKey<string>('req');
|
||||
|
||||
@@ -104,6 +106,10 @@ export function getRequest(transferState: TransferState): any {
|
||||
provide: GoogleAnalyticsService,
|
||||
useClass: GoogleAnalyticsService,
|
||||
},
|
||||
{
|
||||
provide: AuthRequestService,
|
||||
useClass: BrowserAuthRequestService,
|
||||
},
|
||||
{
|
||||
provide: LocationToken,
|
||||
useFactory: locationProvider,
|
||||
|
@@ -31,6 +31,8 @@ import { ServerHardRedirectService } from '../../app/core/services/server-hard-r
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mock';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
|
||||
import { ServerAuthRequestService } from '../../app/core/auth/server-auth-request.service';
|
||||
|
||||
export function createTranslateLoader() {
|
||||
return new TranslateJson5UniversalLoader('dist/server/assets/i18n/', '.json5');
|
||||
@@ -82,6 +84,10 @@ export function createTranslateLoader() {
|
||||
provide: SubmissionService,
|
||||
useClass: ServerSubmissionService
|
||||
},
|
||||
{
|
||||
provide: AuthRequestService,
|
||||
useClass: ServerAuthRequestService,
|
||||
},
|
||||
{
|
||||
provide: LocaleService,
|
||||
useClass: ServerLocaleService
|
||||
|
Reference in New Issue
Block a user