mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
fix(core): limit DateHeaderTimeSyncInterceptor to IAM API responses only
This commit is contained in:
@@ -20,19 +20,27 @@ import { HttpTestingController, provideHttpClientTesting } from '@angular/common
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { TimeSyncService } from '../services/time-sync.service';
|
import { TimeSyncService } from '../services/time-sync.service';
|
||||||
import { DateHeaderTimeSyncInterceptor } from './date-header-time-sync.interceptor';
|
import { DateHeaderTimeSyncInterceptor } from './date-header-time-sync.interceptor';
|
||||||
|
import { AppConfigService } from '../../app-config/app-config.service';
|
||||||
|
|
||||||
describe('DateHeaderTimeSyncInterceptor', () => {
|
describe('DateHeaderTimeSyncInterceptor', () => {
|
||||||
let httpMock: HttpTestingController;
|
let httpMock: HttpTestingController;
|
||||||
let timeSyncServiceSpy: jasmine.SpyObj<TimeSyncService>;
|
let timeSyncServiceSpy: jasmine.SpyObj<TimeSyncService>;
|
||||||
let httpClient: HttpClient;
|
let httpClient: HttpClient;
|
||||||
|
let appConfigServiceMock: jasmine.SpyObj<AppConfigService>;
|
||||||
|
|
||||||
|
const IAM_HOST = 'https://iam.example.com/auth/realms/alfresco';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
timeSyncServiceSpy = jasmine.createSpyObj('TimeSyncService', ['updateClockOffsetFromDateHeader']);
|
timeSyncServiceSpy = jasmine.createSpyObj('TimeSyncService', ['updateClockOffsetFromDateHeader']);
|
||||||
|
appConfigServiceMock = jasmine.createSpyObj('AppConfigService', ['get'], {
|
||||||
|
oauth2: { host: IAM_HOST }
|
||||||
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
DateHeaderTimeSyncInterceptor,
|
DateHeaderTimeSyncInterceptor,
|
||||||
{ provide: TimeSyncService, useValue: timeSyncServiceSpy },
|
{ provide: TimeSyncService, useValue: timeSyncServiceSpy },
|
||||||
|
{ provide: AppConfigService, useValue: appConfigServiceMock },
|
||||||
{ provide: HTTP_INTERCEPTORS, useClass: DateHeaderTimeSyncInterceptor, multi: true },
|
{ provide: HTTP_INTERCEPTORS, useClass: DateHeaderTimeSyncInterceptor, multi: true },
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting()
|
provideHttpClientTesting()
|
||||||
@@ -47,33 +55,56 @@ describe('DateHeaderTimeSyncInterceptor', () => {
|
|||||||
httpMock.verify();
|
httpMock.verify();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call updateClockOffsetFromDateHeader when response contains a Date header', () => {
|
it('should call updateClockOffsetFromDateHeader when IAM response contains a Date header', () => {
|
||||||
const requestStartTime = 1728911579000;
|
const requestStartTime = 1728911579000;
|
||||||
spyOn(Date, 'now').and.returnValue(requestStartTime);
|
spyOn(Date, 'now').and.returnValue(requestStartTime);
|
||||||
|
|
||||||
|
const iamUrl = `${IAM_HOST}/protocol/openid-connect/token`;
|
||||||
|
httpClient.get(iamUrl).subscribe();
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(iamUrl);
|
||||||
|
req.flush({}, { headers: { date: 'Mon, 14 Oct 2024 13:12:59 GMT' } });
|
||||||
|
|
||||||
|
expect(timeSyncServiceSpy.updateClockOffsetFromDateHeader).toHaveBeenCalledWith('Mon, 14 Oct 2024 13:12:59 GMT', requestStartTime);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call updateClockOffsetFromDateHeader for non-IAM URLs', () => {
|
||||||
|
httpClient.get('/api/content/nodes').subscribe();
|
||||||
|
|
||||||
|
const req = httpMock.expectOne('/api/content/nodes');
|
||||||
|
req.flush({}, { headers: { date: 'Mon, 14 Oct 2024 13:12:59 GMT' } });
|
||||||
|
|
||||||
|
expect(timeSyncServiceSpy.updateClockOffsetFromDateHeader).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call updateClockOffsetFromDateHeader when IAM response has no Date header', () => {
|
||||||
|
const iamUrl = `${IAM_HOST}/protocol/openid-connect/token`;
|
||||||
|
httpClient.get(iamUrl).subscribe();
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(iamUrl);
|
||||||
|
req.flush({});
|
||||||
|
|
||||||
|
expect(timeSyncServiceSpy.updateClockOffsetFromDateHeader).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call updateClockOffsetFromDateHeader when oauth2 host is not configured', () => {
|
||||||
|
(Object.getOwnPropertyDescriptor(appConfigServiceMock, 'oauth2')?.get as jasmine.Spy).and.returnValue({ host: '' });
|
||||||
|
|
||||||
httpClient.get('/test').subscribe();
|
httpClient.get('/test').subscribe();
|
||||||
|
|
||||||
const req = httpMock.expectOne('/test');
|
const req = httpMock.expectOne('/test');
|
||||||
req.flush({}, { headers: { date: 'Mon, 14 Oct 2024 13:12:59 GMT' } });
|
req.flush({}, { headers: { date: 'Mon, 14 Oct 2024 13:12:59 GMT' } });
|
||||||
|
|
||||||
expect(timeSyncServiceSpy.updateClockOffsetFromDateHeader).toHaveBeenCalledWith('Mon, 14 Oct 2024 13:12:59 GMT', requestStartTime);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not call updateClockOffsetFromDateHeader when response has no Date header', () => {
|
|
||||||
httpClient.get('/test').subscribe();
|
|
||||||
|
|
||||||
const req = httpMock.expectOne('/test');
|
|
||||||
req.flush({});
|
|
||||||
|
|
||||||
expect(timeSyncServiceSpy.updateClockOffsetFromDateHeader).not.toHaveBeenCalled();
|
expect(timeSyncServiceSpy.updateClockOffsetFromDateHeader).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass through the request unchanged', () => {
|
it('should pass through the request unchanged', () => {
|
||||||
httpClient.get('/test').subscribe();
|
const iamUrl = `${IAM_HOST}/protocol/openid-connect/token`;
|
||||||
|
httpClient.get(iamUrl).subscribe();
|
||||||
|
|
||||||
const req = httpMock.expectOne('/test');
|
const req = httpMock.expectOne(iamUrl);
|
||||||
expect(req.request.method).toBe('GET');
|
expect(req.request.method).toBe('GET');
|
||||||
expect(req.request.url).toBe('/test');
|
expect(req.request.url).toBe(iamUrl);
|
||||||
req.flush({ data: 'value' });
|
req.flush({ data: 'value' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,26 +20,31 @@ import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } fr
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import { tap } from 'rxjs/operators';
|
||||||
import { TimeSyncService } from '../services/time-sync.service';
|
import { TimeSyncService } from '../services/time-sync.service';
|
||||||
|
import { AppConfigService } from '../../app-config/app-config.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP interceptor that passively keeps the clock offset in `TimeSyncService` up-to-date
|
* HTTP interceptor that passively keeps the clock offset in `TimeSyncService` up-to-date
|
||||||
* by reading the standard `Date` response header (RFC 7231) from every HTTP response.
|
* by reading the standard `Date` response header (RFC 7231) from IAM API responses.
|
||||||
*
|
*
|
||||||
* This removes the need for a dedicated `serverTimeUrl` endpoint: as long as HTTP responses
|
* Only responses whose URL starts with the configured OAuth2 host (`oauth2.host`) are
|
||||||
* include a `Date` header (all well-behaved HTTP/1.1 and HTTP/2 servers do), the clock drift
|
* processed, avoiding unnecessary offset recalculations on every HTTP call.
|
||||||
* correction will be applied transparently without an extra network round-trip.
|
*
|
||||||
|
* This removes the need for a dedicated `serverTimeUrl` endpoint: as long as IAM responses
|
||||||
|
* include a `Date` header, the clock drift correction will be applied transparently
|
||||||
|
* without an extra network round-trip.
|
||||||
*
|
*
|
||||||
* The interceptor is registered automatically when `provideCoreAuth()` is used.
|
* The interceptor is registered automatically when `provideCoreAuth()` is used.
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DateHeaderTimeSyncInterceptor implements HttpInterceptor {
|
export class DateHeaderTimeSyncInterceptor implements HttpInterceptor {
|
||||||
private readonly _timeSyncService = inject(TimeSyncService);
|
private readonly _timeSyncService = inject(TimeSyncService);
|
||||||
|
private readonly _appConfigService = inject(AppConfigService);
|
||||||
|
|
||||||
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||||
const requestStartTime = Date.now();
|
const requestStartTime = Date.now();
|
||||||
return next.handle(request).pipe(
|
return next.handle(request).pipe(
|
||||||
tap((event) => {
|
tap((event) => {
|
||||||
if (event instanceof HttpResponse) {
|
if (event instanceof HttpResponse && this.isIamRequest(event.url ?? request.url)) {
|
||||||
const dateHeader = event.headers.get('date');
|
const dateHeader = event.headers.get('date');
|
||||||
if (dateHeader) {
|
if (dateHeader) {
|
||||||
this._timeSyncService.updateClockOffsetFromDateHeader(dateHeader, requestStartTime);
|
this._timeSyncService.updateClockOffsetFromDateHeader(dateHeader, requestStartTime);
|
||||||
@@ -48,4 +53,9 @@ export class DateHeaderTimeSyncInterceptor implements HttpInterceptor {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isIamRequest(url: string): boolean {
|
||||||
|
const iamHost = this._appConfigService.oauth2?.host;
|
||||||
|
return !!iamHost && url.startsWith(iamHost);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user