mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 10:34:15 +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 { Observable } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import {
|
import { GetRequest, PostRequest, RestRequest, } from '../data/request.models';
|
||||||
GetRequest,
|
|
||||||
PostRequest,
|
|
||||||
RestRequest,
|
|
||||||
} from '../data/request.models';
|
|
||||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
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 { ShortLivedToken } from './models/short-lived-token.model';
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
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 linkName = 'authn';
|
||||||
protected browseEndpoint = '';
|
protected browseEndpoint = '';
|
||||||
protected shortlivedtokensEndpoint = 'shortlivedtokens';
|
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> {
|
public getShortlivedToken(): Observable<string> {
|
||||||
return this.halService.getEndpoint(this.linkName).pipe(
|
return this.halService.getEndpoint(this.linkName).pipe(
|
||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
map((href: string) => new URLCombiner(href, this.shortlivedtokensEndpoint).toString()),
|
map((href: string) => new URLCombiner(href, this.shortlivedtokensEndpoint).toString()),
|
||||||
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)),
|
map((endpointURL: string) => this.createShortLivedTokenRequest(endpointURL)),
|
||||||
tap((request: PostRequest) => this.requestService.send(request)),
|
tap((request: RestRequest) => this.requestService.send(request)),
|
||||||
switchMap((request: PostRequest) => this.rdbService.buildFromRequestUUID<ShortLivedToken>(request.uuid)),
|
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<ShortLivedToken>(request.uuid)),
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
map((response: RemoteData<ShortLivedToken>) => {
|
map((response: RemoteData<ShortLivedToken>) => {
|
||||||
if (response.hasSucceeded) {
|
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 { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||||
import { UploaderService } from '../shared/uploader/uploader.service';
|
import { UploaderService } from '../shared/uploader/uploader.service';
|
||||||
import { SectionFormOperationsService } from '../submission/sections/form/section-form-operations.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 { AuthenticatedGuard } from './auth/authenticated.guard';
|
||||||
import { AuthStatus } from './auth/models/auth-status.model';
|
import { AuthStatus } from './auth/models/auth-status.model';
|
||||||
import { BrowseService } from './browse/browse.service';
|
import { BrowseService } from './browse/browse.service';
|
||||||
@@ -188,7 +187,6 @@ const EXPORTS = [];
|
|||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
ApiService,
|
ApiService,
|
||||||
AuthenticatedGuard,
|
AuthenticatedGuard,
|
||||||
AuthRequestService,
|
|
||||||
CommunityDataService,
|
CommunityDataService,
|
||||||
CollectionDataService,
|
CollectionDataService,
|
||||||
SiteDataService,
|
SiteDataService,
|
||||||
|
@@ -31,6 +31,8 @@ import {
|
|||||||
import { LocaleService } from '../../app/core/locale/locale.service';
|
import { LocaleService } from '../../app/core/locale/locale.service';
|
||||||
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
||||||
import { RouterModule, NoPreloading } from '@angular/router';
|
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');
|
export const REQ_KEY = makeStateKey<string>('req');
|
||||||
|
|
||||||
@@ -104,6 +106,10 @@ export function getRequest(transferState: TransferState): any {
|
|||||||
provide: GoogleAnalyticsService,
|
provide: GoogleAnalyticsService,
|
||||||
useClass: GoogleAnalyticsService,
|
useClass: GoogleAnalyticsService,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: AuthRequestService,
|
||||||
|
useClass: BrowserAuthRequestService,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: LocationToken,
|
provide: LocationToken,
|
||||||
useFactory: locationProvider,
|
useFactory: locationProvider,
|
||||||
|
@@ -31,6 +31,8 @@ import { ServerHardRedirectService } from '../../app/core/services/server-hard-r
|
|||||||
import { Angulartics2 } from 'angulartics2';
|
import { Angulartics2 } from 'angulartics2';
|
||||||
import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mock';
|
import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mock';
|
||||||
import { RouterModule } from '@angular/router';
|
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() {
|
export function createTranslateLoader() {
|
||||||
return new TranslateJson5UniversalLoader('dist/server/assets/i18n/', '.json5');
|
return new TranslateJson5UniversalLoader('dist/server/assets/i18n/', '.json5');
|
||||||
@@ -82,6 +84,10 @@ export function createTranslateLoader() {
|
|||||||
provide: SubmissionService,
|
provide: SubmissionService,
|
||||||
useClass: ServerSubmissionService
|
useClass: ServerSubmissionService
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: AuthRequestService,
|
||||||
|
useClass: ServerAuthRequestService,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: LocaleService,
|
provide: LocaleService,
|
||||||
useClass: ServerLocaleService
|
useClass: ServerLocaleService
|
||||||
|
Reference in New Issue
Block a user